diff options
| author | sangtran <tranquangsang12.7@gmail.com> | 2026-08-29 01:08:28 +0700 |
|---|---|---|
| committer | sangtran <tranquangsang12.7@gmail.com> | 2026-08-29 01:08:28 +0700 |
| commit | 96c07bab1086510a55a004e9a1b90c30bc723a5c (patch) | |
| tree | 7abbc615c51667e111fdef7b2ef69a9fc663f2ca | |
| signature | Verified sangtran <tranquangsang12.7@gmail.com> Key: 8DD79823D235E1BB ultimate trust | |
| download | flux-96c07bab1086510a55a004e9a1b90c30bc723a5c.tar.gz flux-96c07bab1086510a55a004e9a1b90c30bc723a5c.zip | |
Verified feat: init base draft support window
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | README.md | 14 | ||||
| -rw-r--r-- | core/hash.go | 19 | ||||
| -rw-r--r-- | core/operator/filter.go | 36 | ||||
| -rw-r--r-- | core/operator/keyby.go | 32 | ||||
| -rw-r--r-- | core/operator/keyed_process.go | 81 | ||||
| -rw-r--r-- | core/operator/map.go | 53 | ||||
| -rw-r--r-- | core/operator/operator.go | 16 | ||||
| -rw-r--r-- | core/operator/stateful_map.go | 75 | ||||
| -rw-r--r-- | core/operator/watermark.go | 61 | ||||
| -rw-r--r-- | core/operator/window.go | 110 | ||||
| -rw-r--r-- | core/record.go | 40 | ||||
| -rw-r--r-- | core/state.go | 10 | ||||
| -rw-r--r-- | core/state_backend.go | 62 | ||||
| -rw-r--r-- | core/timer.go | 113 | ||||
| -rw-r--r-- | core/window.go | 54 | ||||
| -rw-r--r-- | go.mod | 3 | ||||
| -rw-r--r-- | main.go | 96 | ||||
| -rw-r--r-- | runtime/task.go | 66 | ||||
| -rw-r--r-- | state/memory_backend.go | 57 | ||||
| -rw-r--r-- | state/memory_state.go | 44 | ||||
| -rw-r--r-- | transport/channel.go | 23 | ||||
| -rw-r--r-- | transport/local_channel.go | 56 |
23 files changed, 1123 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68729d6 --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | .idea/ | ||
| 2 | .DS_Store \ No newline at end of file | ||
diff --git a/README.md b/README.md new file mode 100644 index 0000000..d947411 --- /dev/null +++ b/README.md | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | # flux | ||
| 2 | |||
| 3 | |||
| 4 | |||
| 5 | |||
| 6 | > [!NOTE] | ||
| 7 | > Namespace should be interface instead of any/interface{} because it cost boxing & GC heavy things and also losing the type safety | ||
| 8 | > | ||
| 9 | |||
| 10 | |||
| 11 | |||
| 12 | ### Namespace improvement | ||
| 13 | Should serialize to raw []byte to archive serialize speed, flexible, zero GC(with buffer bool) also compatible with PebbleDB | ||
| 14 | |||
diff --git a/core/hash.go b/core/hash.go new file mode 100644 index 0000000..af15546 --- /dev/null +++ b/core/hash.go | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | package core | ||
| 2 | |||
| 3 | import "hash/fnv" | ||
| 4 | |||
| 5 | const DefaultMaxParallelism = 128 | ||
| 6 | |||
| 7 | func AssignToKeyGroup(key string, maxParallelism int) int { | ||
| 8 | |||
| 9 | if maxParallelism <= 0 { | ||
| 10 | maxParallelism = DefaultMaxParallelism | ||
| 11 | } | ||
| 12 | |||
| 13 | // hash | ||
| 14 | h := fnv.New32a() | ||
| 15 | h.Write([]byte(key)) | ||
| 16 | hash := h.Sum32() | ||
| 17 | |||
| 18 | return int(hash % uint32(maxParallelism)) | ||
| 19 | } | ||
diff --git a/core/operator/filter.go b/core/operator/filter.go new file mode 100644 index 0000000..80bbb77 --- /dev/null +++ b/core/operator/filter.go | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | package operator | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "flux/core" | ||
| 6 | ) | ||
| 7 | |||
| 8 | type FilterFunc[IN any] func(in IN) bool | ||
| 9 | |||
| 10 | type FilterOperator[IN any] struct { | ||
| 11 | userFunc FilterFunc[IN] | ||
| 12 | } | ||
| 13 | |||
| 14 | func NewFilterOperator[IN any](userFunc FilterFunc[IN]) *FilterOperator[IN] { | ||
| 15 | return &FilterOperator[IN]{userFunc: userFunc} | ||
| 16 | } | ||
| 17 | |||
| 18 | func (f *FilterOperator[IN]) Open(ctx context.Context) error { | ||
| 19 | return nil | ||
| 20 | } | ||
| 21 | |||
| 22 | func (f *FilterOperator[IN]) Process(ctx context.Context, record core.StreamRecord[IN], out Emitter[IN]) error { | ||
| 23 | if record.Type != core.RecordData { | ||
| 24 | return out.Emit(ctx, record) | ||
| 25 | } | ||
| 26 | |||
| 27 | if f.userFunc(record.Value) { | ||
| 28 | return out.Emit(ctx, record) | ||
| 29 | } | ||
| 30 | |||
| 31 | return nil | ||
| 32 | } | ||
| 33 | |||
| 34 | func (f *FilterOperator[IN]) Close(ctx context.Context) error { | ||
| 35 | return nil | ||
| 36 | } | ||
diff --git a/core/operator/keyby.go b/core/operator/keyby.go new file mode 100644 index 0000000..56ef34e --- /dev/null +++ b/core/operator/keyby.go | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | package operator | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "flux/core" | ||
| 6 | ) | ||
| 7 | |||
| 8 | type KeySelector[IN any] func(in IN) string | ||
| 9 | type KeyByOperator[IN any] struct { | ||
| 10 | selector KeySelector[IN] | ||
| 11 | } | ||
| 12 | |||
| 13 | func NewKeyByOperator[IN any](selector KeySelector[IN]) *KeyByOperator[IN] { | ||
| 14 | return &KeyByOperator[IN]{selector: selector} | ||
| 15 | } | ||
| 16 | |||
| 17 | func (k *KeyByOperator[IN]) Open(ctx context.Context) error { | ||
| 18 | return nil | ||
| 19 | } | ||
| 20 | |||
| 21 | func (k *KeyByOperator[IN]) Process(ctx context.Context, record core.StreamRecord[IN], out Emitter[IN]) error { | ||
| 22 | if record.Type != core.RecordData { | ||
| 23 | return out.Emit(ctx, record) | ||
| 24 | } | ||
| 25 | |||
| 26 | record.Key = k.selector(record.Value) | ||
| 27 | return out.Emit(ctx, record) | ||
| 28 | } | ||
| 29 | |||
| 30 | func (k *KeyByOperator[IN]) Close(ctx context.Context) error { | ||
| 31 | return nil | ||
| 32 | } | ||
diff --git a/core/operator/keyed_process.go b/core/operator/keyed_process.go new file mode 100644 index 0000000..1d3c86c --- /dev/null +++ b/core/operator/keyed_process.go | |||
| @@ -0,0 +1,81 @@ | |||
| 1 | package operator | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "flux/core" | ||
| 6 | "fmt" | ||
| 7 | ) | ||
| 8 | |||
| 9 | type KeyedProcesser[IN, OUT any] interface { | ||
| 10 | Open(ctx context.Context) error | ||
| 11 | ProcessElement(ctx context.Context, record core.StreamRecord[IN], out Emitter[OUT]) error | ||
| 12 | Close(ctx context.Context) error | ||
| 13 | OnTimer(ctx context.Context, timestamp int64, out Emitter[OUT]) error | ||
| 14 | } | ||
| 15 | |||
| 16 | type KeyProcessOperator[IN, OUT any] struct { | ||
| 17 | userFunc KeyedProcesser[IN, OUT] | ||
| 18 | timeService core.InternalTimerService | ||
| 19 | backend core.StateBackend | ||
| 20 | } | ||
| 21 | |||
| 22 | func NewKeyProcessOperator[IN, OUT any](userFunc KeyedProcesser[IN, OUT]) *KeyProcessOperator[IN, OUT] { | ||
| 23 | return &KeyProcessOperator[IN, OUT]{ | ||
| 24 | userFunc: userFunc, | ||
| 25 | timeService: core.NewTimerService(), | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | func (k *KeyProcessOperator[IN, OUT]) Open(ctx context.Context) error { | ||
| 30 | // init backend | ||
| 31 | be, ok := core.ExtractStateBackend(ctx) | ||
| 32 | if !ok { | ||
| 33 | return fmt.Errorf("state backend not found") | ||
| 34 | } | ||
| 35 | |||
| 36 | k.backend = be | ||
| 37 | // inject the timer to context for user use | ||
| 38 | ctx = core.InjectTimerService(ctx, k.timeService) | ||
| 39 | return k.userFunc.Open(ctx) | ||
| 40 | |||
| 41 | } | ||
| 42 | |||
| 43 | func (k *KeyProcessOperator[IN, OUT]) Process(ctx context.Context, record core.StreamRecord[IN], out Emitter[OUT]) error { | ||
| 44 | |||
| 45 | switch record.Type { | ||
| 46 | case core.RecordData: | ||
| 47 | if err := k.backend.SetCurrentKey(record.Key); err != nil { | ||
| 48 | return err | ||
| 49 | } | ||
| 50 | |||
| 51 | return k.userFunc.ProcessElement(ctx, record, out) | ||
| 52 | case core.RecordWatermark: | ||
| 53 | // advance the timer | ||
| 54 | triggerTimerQueue := k.timeService.AdvanceWatermark(record.Timestamp) | ||
| 55 | |||
| 56 | for _, t := range triggerTimerQueue { | ||
| 57 | |||
| 58 | if err := k.backend.SetCurrentKey(t.Key); err != nil { | ||
| 59 | return err | ||
| 60 | } | ||
| 61 | if err := k.userFunc.OnTimer(ctx, t.Timestamp, out); err != nil { | ||
| 62 | return err | ||
| 63 | } | ||
| 64 | } | ||
| 65 | return out.Emit(ctx, core.StreamRecord[OUT]{ | ||
| 66 | Type: core.RecordWatermark, | ||
| 67 | Timestamp: record.Timestamp, | ||
| 68 | }) | ||
| 69 | default: | ||
| 70 | return out.Emit(ctx, core.StreamRecord[OUT]{ | ||
| 71 | Timestamp: record.Timestamp, | ||
| 72 | BarrierID: record.BarrierID, | ||
| 73 | Key: record.Key, | ||
| 74 | }) | ||
| 75 | } | ||
| 76 | |||
| 77 | } | ||
| 78 | |||
| 79 | func (k *KeyProcessOperator[IN, OUT]) Close(ctx context.Context) error { | ||
| 80 | return k.userFunc.Close(ctx) | ||
| 81 | } | ||
diff --git a/core/operator/map.go b/core/operator/map.go new file mode 100644 index 0000000..2165c25 --- /dev/null +++ b/core/operator/map.go | |||
| @@ -0,0 +1,53 @@ | |||
| 1 | package operator | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "flux/core" | ||
| 6 | "fmt" | ||
| 7 | ) | ||
| 8 | |||
| 9 | // MapFunc is business logic for user implement | ||
| 10 | type MapFunc[IN, OUT any] func(in IN) (OUT, error) | ||
| 11 | |||
| 12 | type MapOperator[IN, OUT any] struct { | ||
| 13 | userFunc MapFunc[IN, OUT] | ||
| 14 | } | ||
| 15 | |||
| 16 | func NewMapOperator[IN, OUT any](fn MapFunc[IN, OUT]) *MapOperator[IN, OUT] { | ||
| 17 | return &MapOperator[IN, OUT]{ | ||
| 18 | userFunc: fn, | ||
| 19 | } | ||
| 20 | } | ||
| 21 | |||
| 22 | func (fo *MapOperator[IN, OUT]) Open(ctx context.Context) error { | ||
| 23 | return nil | ||
| 24 | } | ||
| 25 | |||
| 26 | func (fo *MapOperator[IN, OUT]) Process(ctx context.Context, record core.StreamRecord[IN], out Emitter[OUT]) error { | ||
| 27 | if record.Type != core.RecordData { | ||
| 28 | // watermark and barrier | ||
| 29 | return out.Emit(ctx, core.StreamRecord[OUT]{ | ||
| 30 | Type: record.Type, | ||
| 31 | Key: record.Key, | ||
| 32 | Timestamp: record.Timestamp, | ||
| 33 | BarrierID: record.BarrierID, | ||
| 34 | }) | ||
| 35 | } | ||
| 36 | |||
| 37 | outValue, err := fo.userFunc(record.Value) | ||
| 38 | |||
| 39 | if err != nil { | ||
| 40 | return fmt.Errorf("cannot process value %v: %w", record.Value, err) | ||
| 41 | } | ||
| 42 | |||
| 43 | return out.Emit(ctx, core.StreamRecord[OUT]{ | ||
| 44 | Type: core.RecordData, | ||
| 45 | Key: record.Key, | ||
| 46 | Timestamp: record.Timestamp, | ||
| 47 | Value: outValue, | ||
| 48 | }) | ||
| 49 | } | ||
| 50 | |||
| 51 | func (fo *MapOperator[IN, OUT]) Close(ctx context.Context) error { | ||
| 52 | return nil | ||
| 53 | } | ||
diff --git a/core/operator/operator.go b/core/operator/operator.go new file mode 100644 index 0000000..0c588e5 --- /dev/null +++ b/core/operator/operator.go | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | package operator | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "flux/core" | ||
| 6 | ) | ||
| 7 | |||
| 8 | type Emitter[OUT any] interface { | ||
| 9 | Emit(ctx context.Context, record core.StreamRecord[OUT]) error | ||
| 10 | } | ||
| 11 | |||
| 12 | type Operator[IN, OUT any] interface { | ||
| 13 | Open(ctx context.Context) error | ||
| 14 | Process(ctx context.Context, record core.StreamRecord[IN], out Emitter[OUT]) error | ||
| 15 | Close(ctx context.Context) error | ||
| 16 | } | ||
diff --git a/core/operator/stateful_map.go b/core/operator/stateful_map.go new file mode 100644 index 0000000..295c656 --- /dev/null +++ b/core/operator/stateful_map.go | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | package operator | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "fmt" | ||
| 6 | |||
| 7 | "flux/core" | ||
| 8 | ) | ||
| 9 | |||
| 10 | // RichMapFunc is a stateful map on the user side: Open grabs the state handles | ||
| 11 | // off ctx, Map runs per record with the backend already pointed at that | ||
| 12 | // record's key. | ||
| 13 | type RichMapFunc[IN, OUT any] interface { | ||
| 14 | Open(ctx context.Context) error | ||
| 15 | Map(ctx context.Context, in IN) (OUT, error) | ||
| 16 | Close(ctx context.Context) error | ||
| 17 | } | ||
| 18 | |||
| 19 | type StatefulMapOperator[IN, OUT any] struct { | ||
| 20 | userFunc RichMapFunc[IN, OUT] | ||
| 21 | backend core.StateBackend | ||
| 22 | } | ||
| 23 | |||
| 24 | func NewStatefulMapOperator[IN, OUT any](fn RichMapFunc[IN, OUT]) *StatefulMapOperator[IN, OUT] { | ||
| 25 | return &StatefulMapOperator[IN, OUT]{userFunc: fn} | ||
| 26 | } | ||
| 27 | |||
| 28 | func (s *StatefulMapOperator[IN, OUT]) Open(ctx context.Context) error { | ||
| 29 | // Get backend state from context via using context KV | ||
| 30 | be, ok := core.ExtractStateBackend(ctx) | ||
| 31 | if !ok || be == nil { | ||
| 32 | return fmt.Errorf("stateful map: no state backend in context") | ||
| 33 | } | ||
| 34 | |||
| 35 | s.backend = be | ||
| 36 | // The user func opens its ValueState off the same ctx. | ||
| 37 | return s.userFunc.Open(ctx) | ||
| 38 | } | ||
| 39 | |||
| 40 | func (s *StatefulMapOperator[IN, OUT]) Process(ctx context.Context, record core.StreamRecord[IN], out Emitter[OUT]) error { | ||
| 41 | if record.Type != core.RecordData { | ||
| 42 | return out.Emit(ctx, core.StreamRecord[OUT]{ | ||
| 43 | Type: record.Type, | ||
| 44 | Timestamp: record.Timestamp, | ||
| 45 | Key: record.Key, | ||
| 46 | BarrierID: record.BarrierID, | ||
| 47 | }) | ||
| 48 | } | ||
| 49 | |||
| 50 | // Without a key every record would silently share the "" state slot. | ||
| 51 | if record.Key == "" { | ||
| 52 | return fmt.Errorf("stateful map: record %v has no key, keyBy it first", record.Value) | ||
| 53 | } | ||
| 54 | |||
| 55 | if err := s.backend.SetCurrentKey(record.Key); err != nil { | ||
| 56 | return fmt.Errorf("set current key %q: %w", record.Key, err) | ||
| 57 | } | ||
| 58 | |||
| 59 | // run the user transform | ||
| 60 | res, err := s.userFunc.Map(ctx, record.Value) | ||
| 61 | if err != nil { | ||
| 62 | return fmt.Errorf("cannot process value %v: %w", record.Value, err) | ||
| 63 | } | ||
| 64 | |||
| 65 | return out.Emit(ctx, core.StreamRecord[OUT]{ | ||
| 66 | Type: core.RecordData, | ||
| 67 | Timestamp: record.Timestamp, | ||
| 68 | Key: record.Key, | ||
| 69 | Value: res, | ||
| 70 | }) | ||
| 71 | } | ||
| 72 | |||
| 73 | func (s *StatefulMapOperator[IN, OUT]) Close(ctx context.Context) error { | ||
| 74 | return s.userFunc.Close(ctx) | ||
| 75 | } | ||
diff --git a/core/operator/watermark.go b/core/operator/watermark.go new file mode 100644 index 0000000..3d591e5 --- /dev/null +++ b/core/operator/watermark.go | |||
| @@ -0,0 +1,61 @@ | |||
| 1 | package operator | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | |||
| 6 | "flux/core" | ||
| 7 | ) | ||
| 8 | |||
| 9 | // TimestampAssigner is responsible for event time defined by user | ||
| 10 | type TimestampAssigner[IN any] func(in IN) int64 | ||
| 11 | |||
| 12 | type WaterGeneratorOperator[IN any] struct { | ||
| 13 | assigner TimestampAssigner[IN] | ||
| 14 | maxOutOfOrder int64 | ||
| 15 | currentMaxTimestamp int64 | ||
| 16 | lastEmittedWatermark int64 | ||
| 17 | } | ||
| 18 | |||
| 19 | func NewWatermarkGeneratorOperator[IN any](maxOutOfOrder int64, assigner TimestampAssigner[IN]) *WaterGeneratorOperator[IN] { | ||
| 20 | return &WaterGeneratorOperator[IN]{ | ||
| 21 | maxOutOfOrder: maxOutOfOrder, | ||
| 22 | assigner: assigner, | ||
| 23 | lastEmittedWatermark: -1, | ||
| 24 | } | ||
| 25 | } | ||
| 26 | |||
| 27 | func (w *WaterGeneratorOperator[IN]) Open(ctx context.Context) error { | ||
| 28 | return nil | ||
| 29 | } | ||
| 30 | |||
| 31 | func (w *WaterGeneratorOperator[IN]) Process(ctx context.Context, record core.StreamRecord[IN], out Emitter[IN]) error { | ||
| 32 | if record.Type == core.RecordData { | ||
| 33 | if err := out.Emit(ctx, record); err != nil { | ||
| 34 | return err | ||
| 35 | } | ||
| 36 | // query the time | ||
| 37 | eventTime := w.assigner(record.Value) | ||
| 38 | record.Timestamp = eventTime | ||
| 39 | |||
| 40 | // update the max timestamp | ||
| 41 | if w.currentMaxTimestamp < eventTime { | ||
| 42 | w.currentMaxTimestamp = eventTime | ||
| 43 | } | ||
| 44 | |||
| 45 | // calc the watermark | ||
| 46 | |||
| 47 | watermark := w.currentMaxTimestamp - w.maxOutOfOrder | ||
| 48 | |||
| 49 | // check if current watermark later than previous watermark | ||
| 50 | if watermark > w.lastEmittedWatermark { | ||
| 51 | w.lastEmittedWatermark = watermark | ||
| 52 | return out.Emit(ctx, core.NewWatermarkRecord[IN](watermark)) | ||
| 53 | } | ||
| 54 | return nil | ||
| 55 | } | ||
| 56 | return out.Emit(ctx, record) | ||
| 57 | } | ||
| 58 | |||
| 59 | func (w *WaterGeneratorOperator[IN]) Close(ctx context.Context) error { | ||
| 60 | return nil | ||
| 61 | } | ||
diff --git a/core/operator/window.go b/core/operator/window.go new file mode 100644 index 0000000..610f6e3 --- /dev/null +++ b/core/operator/window.go | |||
| @@ -0,0 +1,110 @@ | |||
| 1 | package operator | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "fmt" | ||
| 6 | |||
| 7 | "flux/core" | ||
| 8 | ) | ||
| 9 | |||
| 10 | // WindowFunction is for user interface implements the behaviour | ||
| 11 | type WindowFunction[IN, OUT any] interface { | ||
| 12 | Apply(ctx context.Context, key string, window core.Window, in []IN, out Emitter[OUT]) error | ||
| 13 | } | ||
| 14 | |||
| 15 | type WindowOperator[IN, OUT any] struct { | ||
| 16 | assigner core.WindowAssigner[IN] | ||
| 17 | userfunc WindowFunction[IN, OUT] | ||
| 18 | backend core.StateBackend | ||
| 19 | timeSerivce core.InternalTimerService | ||
| 20 | |||
| 21 | windowState core.ValueState[[]IN] | ||
| 22 | } | ||
| 23 | |||
| 24 | func NewWindowOperator[IN, OUT any](assigner core.WindowAssigner[IN], userfunc WindowFunction[IN, OUT]) *WindowOperator[IN, OUT] { | ||
| 25 | return &WindowOperator[IN, OUT]{ | ||
| 26 | assigner: assigner, | ||
| 27 | userfunc: userfunc, | ||
| 28 | timeSerivce: core.NewTimerService(), | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | func (o *WindowOperator[IN, OUT]) Open(ctx context.Context) error { | ||
| 33 | be, ok := core.ExtractStateBackend(ctx) | ||
| 34 | if !ok { | ||
| 35 | return fmt.Errorf("cannot extract state backend, got: %v", be) | ||
| 36 | } | ||
| 37 | o.backend = be | ||
| 38 | windowState, err := be.GetValueState(ctx, "window-elements") | ||
| 39 | if err != nil { | ||
| 40 | return fmt.Errorf("cannot get value state window-elements error: %w", err) | ||
| 41 | } | ||
| 42 | |||
| 43 | o.windowState = core.CastValueType[[]IN](windowState) | ||
| 44 | |||
| 45 | return nil | ||
| 46 | } | ||
| 47 | |||
| 48 | func (o *WindowOperator[IN, OUT]) Process(ctx context.Context, record core.StreamRecord[IN], out Emitter[OUT]) error { | ||
| 49 | switch record.Type { | ||
| 50 | case core.RecordData: | ||
| 51 | // current windows state will hold all record in window | ||
| 52 | // assign and get list window | ||
| 53 | windows := o.assigner.AssignWindows(record.Value, record.Timestamp) | ||
| 54 | for _, w := range windows { | ||
| 55 | o.backend.SetCurrentNamespace(w) | ||
| 56 | o.backend.SetCurrentKey(record.Key) | ||
| 57 | o.timeSerivce.Register(w.MaxTimestamp(), record.Key, w) | ||
| 58 | |||
| 59 | currentWindowState, err := o.windowState.Value(ctx) | ||
| 60 | if err != nil { | ||
| 61 | return err | ||
| 62 | } | ||
| 63 | |||
| 64 | currentWindowState = append(currentWindowState, record.Value) | ||
| 65 | |||
| 66 | o.windowState.Update(ctx, currentWindowState) | ||
| 67 | } | ||
| 68 | |||
| 69 | return nil | ||
| 70 | case core.RecordWatermark: | ||
| 71 | deadlines := o.timeSerivce.AdvanceWatermark(record.Timestamp) | ||
| 72 | |||
| 73 | for _, timer := range deadlines { | ||
| 74 | o.backend.SetCurrentKey(timer.Key) | ||
| 75 | o.backend.SetCurrentNamespace(timer.Namespace) | ||
| 76 | |||
| 77 | currentWindowState, err := o.windowState.Value(ctx) | ||
| 78 | if err != nil { | ||
| 79 | return err | ||
| 80 | } | ||
| 81 | |||
| 82 | // NOTE: this is workaround create inline window, refactor later | ||
| 83 | |||
| 84 | if err := o.userfunc.Apply(ctx, timer.Key, timer.Namespace.(core.Window), currentWindowState, out); err != nil { | ||
| 85 | return err | ||
| 86 | } | ||
| 87 | |||
| 88 | if err := o.windowState.Clear(ctx); err != nil { | ||
| 89 | return err | ||
| 90 | } | ||
| 91 | |||
| 92 | } | ||
| 93 | |||
| 94 | return out.Emit(ctx, core.StreamRecord[OUT]{ | ||
| 95 | Timestamp: record.Timestamp, | ||
| 96 | Type: core.RecordWatermark, | ||
| 97 | }) | ||
| 98 | default: | ||
| 99 | return out.Emit(ctx, core.StreamRecord[OUT]{ | ||
| 100 | BarrierID: record.BarrierID, | ||
| 101 | Timestamp: record.Timestamp, | ||
| 102 | Key: record.Key, | ||
| 103 | Type: record.Type, | ||
| 104 | }) | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | func (o *WindowOperator[IN, OUT]) Close(ctx context.Context) error { | ||
| 109 | return nil | ||
| 110 | } | ||
diff --git a/core/record.go b/core/record.go new file mode 100644 index 0000000..e18e58d --- /dev/null +++ b/core/record.go | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | package core | ||
| 2 | |||
| 3 | type RecordType byte | ||
| 4 | |||
| 5 | const ( | ||
| 6 | RecordData RecordType = iota | ||
| 7 | RecordWatermark | ||
| 8 | RecordBarrier | ||
| 9 | ) | ||
| 10 | |||
| 11 | type StreamRecord[T any] struct { | ||
| 12 | Value T | ||
| 13 | Key string // for keyBy stateful processing | ||
| 14 | Timestamp int64 // epoch ms time | ||
| 15 | BarrierID uint64 // ID checkpoint barrier | ||
| 16 | Type RecordType | ||
| 17 | } | ||
| 18 | |||
| 19 | func NewDataRecord[T any](key string, val T, timestamp int64) StreamRecord[T] { | ||
| 20 | return StreamRecord[T]{ | ||
| 21 | Value: val, | ||
| 22 | Key: key, | ||
| 23 | Timestamp: timestamp, | ||
| 24 | Type: RecordData, | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | func NewWatermarkRecord[T any](timestamp int64) StreamRecord[T] { | ||
| 29 | return StreamRecord[T]{ | ||
| 30 | Type: RecordWatermark, | ||
| 31 | Timestamp: timestamp, | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | func NewBarrierRecord[T any](barrierID uint64) StreamRecord[T] { | ||
| 36 | return StreamRecord[T]{ | ||
| 37 | Type: RecordBarrier, | ||
| 38 | BarrierID: barrierID, | ||
| 39 | } | ||
| 40 | } | ||
diff --git a/core/state.go b/core/state.go new file mode 100644 index 0000000..d6d7847 --- /dev/null +++ b/core/state.go | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | package core | ||
| 2 | |||
| 3 | import "context" | ||
| 4 | |||
| 5 | // ValueState is for client side using to update state | ||
| 6 | type ValueState[T any] interface { | ||
| 7 | Value(ctx context.Context) (T, error) | ||
| 8 | Update(ctx context.Context, newVal T) (T, error) | ||
| 9 | Clear(ctx context.Context) error | ||
| 10 | } | ||
diff --git a/core/state_backend.go b/core/state_backend.go new file mode 100644 index 0000000..9f9f397 --- /dev/null +++ b/core/state_backend.go | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | package core | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | ) | ||
| 6 | |||
| 7 | type StateBackend interface { | ||
| 8 | // For Framework === | ||
| 9 | // SetCurrentKey will perform in Memory | ||
| 10 | SetCurrentKey(key string) error | ||
| 11 | // GetCurrentKey will perform in Memory | ||
| 12 | GetCurrentKey() string | ||
| 13 | SetCurrentNamespace(namespace any) error | ||
| 14 | GetCurrentNamespace() any | ||
| 15 | |||
| 16 | // For User === | ||
| 17 | // GetValueState will perform in disk | ||
| 18 | GetValueState(ctx context.Context, key string) (ValueState[any], error) | ||
| 19 | } | ||
| 20 | |||
| 21 | type stateBackendKeyType struct{} | ||
| 22 | |||
| 23 | var stateBackendKey stateBackendKeyType | ||
| 24 | |||
| 25 | type typedValueState[T any] struct { | ||
| 26 | inner ValueState[any] | ||
| 27 | } | ||
| 28 | |||
| 29 | func CastValueType[T any](inner ValueState[any]) ValueState[T] { | ||
| 30 | return &typedValueState[T]{inner: inner} | ||
| 31 | } | ||
| 32 | |||
| 33 | func (s *typedValueState[T]) Value(ctx context.Context) (T, error) { | ||
| 34 | val, err := s.inner.Value(ctx) | ||
| 35 | if err != nil { | ||
| 36 | var zero T | ||
| 37 | return zero, err | ||
| 38 | } | ||
| 39 | if val == nil { | ||
| 40 | var zero T | ||
| 41 | return zero, nil | ||
| 42 | } | ||
| 43 | return val.(T), nil | ||
| 44 | } | ||
| 45 | |||
| 46 | func (s *typedValueState[T]) Update(ctx context.Context, newVal T) (T, error) { | ||
| 47 | _, err := s.inner.Update(ctx, newVal) | ||
| 48 | return newVal, err | ||
| 49 | } | ||
| 50 | |||
| 51 | func (s *typedValueState[T]) Clear(ctx context.Context) error { | ||
| 52 | return s.inner.Clear(ctx) | ||
| 53 | } | ||
| 54 | |||
| 55 | func InjectStateBackend(ctx context.Context, state StateBackend) context.Context { | ||
| 56 | return context.WithValue(ctx, stateBackendKey, state) | ||
| 57 | } | ||
| 58 | |||
| 59 | func ExtractStateBackend(ctx context.Context) (StateBackend, bool) { | ||
| 60 | backend, ok := ctx.Value(stateBackendKey).(StateBackend) | ||
| 61 | return backend, ok | ||
| 62 | } | ||
diff --git a/core/timer.go b/core/timer.go new file mode 100644 index 0000000..3d17299 --- /dev/null +++ b/core/timer.go | |||
| @@ -0,0 +1,113 @@ | |||
| 1 | package core | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "container/heap" | ||
| 5 | "context" | ||
| 6 | ) | ||
| 7 | |||
| 8 | // If using rockDB no need priority queue anymore | ||
| 9 | // implement priority queue from golang | ||
| 10 | // ref: https://pkg.go.dev/container/heap#example-package-PriorityQueue | ||
| 11 | |||
| 12 | type timerServiceKey struct{} | ||
| 13 | |||
| 14 | // InjectTimerService inject the timer into Context | ||
| 15 | func InjectTimerService(ctx context.Context, ts TimerService) context.Context { | ||
| 16 | return context.WithValue(ctx, timerServiceKey{}, ts) | ||
| 17 | } | ||
| 18 | |||
| 19 | // ExtractTimerService get the timer for user from context | ||
| 20 | func ExtractTimerService(ctx context.Context) (TimerService, bool) { | ||
| 21 | ts, ok := ctx.Value(timerServiceKey{}).(TimerService) | ||
| 22 | return ts, ok | ||
| 23 | } | ||
| 24 | |||
| 25 | type Timer struct { | ||
| 26 | // Timestamp is the time when Timer is triggered | ||
| 27 | Timestamp int64 | ||
| 28 | // Key is for who User involve | ||
| 29 | Key string | ||
| 30 | // Window is for time range start-end of | ||
| 31 | // String actually cost more bytes than Window(16bytes) | ||
| 32 | // and we dont want to decode from namespace to Window | ||
| 33 | Namespace any | ||
| 34 | } | ||
| 35 | |||
| 36 | // Priority Queue (Min Heap). Smallest timestamp will be on top | ||
| 37 | |||
| 38 | type timerHeap []Timer | ||
| 39 | |||
| 40 | func (h *timerHeap) Len() int { return len(*h) } | ||
| 41 | func (h *timerHeap) Less(i, j int) bool { return (*h)[i].Timestamp < (*h)[j].Timestamp } | ||
| 42 | func (h *timerHeap) Swap(i, j int) { | ||
| 43 | (*h)[i], (*h)[j] = (*h)[j], (*h)[i] | ||
| 44 | } | ||
| 45 | |||
| 46 | func (h *timerHeap) Push(x any) { | ||
| 47 | *h = append(*h, x.(Timer)) | ||
| 48 | } | ||
| 49 | |||
| 50 | func (h *timerHeap) Pop() any { | ||
| 51 | old := *h | ||
| 52 | n := len(old) | ||
| 53 | last := old[n-1] | ||
| 54 | *h = old[:n-1] | ||
| 55 | return last | ||
| 56 | } | ||
| 57 | |||
| 58 | type TimerService interface { | ||
| 59 | Register(time int64, key string, keyspace any) | ||
| 60 | CurrentWatermark() int64 | ||
| 61 | } | ||
| 62 | |||
| 63 | type InternalTimerService interface { | ||
| 64 | TimerService | ||
| 65 | AdvanceWatermark(watermark int64) []Timer | ||
| 66 | } | ||
| 67 | |||
| 68 | type timerService struct { | ||
| 69 | queue timerHeap | ||
| 70 | currentWatermark int64 | ||
| 71 | } | ||
| 72 | |||
| 73 | func NewTimerService() InternalTimerService { | ||
| 74 | return &timerService{ | ||
| 75 | queue: make(timerHeap, 0), | ||
| 76 | currentWatermark: -1, | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | func (t *timerService) Register(time int64, key string, namespace any) { | ||
| 81 | heap.Push(&t.queue, Timer{ | ||
| 82 | Timestamp: time, | ||
| 83 | Key: key, | ||
| 84 | Namespace: namespace, | ||
| 85 | }) | ||
| 86 | } | ||
| 87 | |||
| 88 | func (t *timerService) CurrentWatermark() int64 { | ||
| 89 | return t.currentWatermark | ||
| 90 | } | ||
| 91 | |||
| 92 | func (t *timerService) AdvanceWatermark(watermark int64) []Timer { | ||
| 93 | if watermark < t.currentWatermark { | ||
| 94 | // no need to advance when get smaller | ||
| 95 | return nil | ||
| 96 | } | ||
| 97 | |||
| 98 | t.currentWatermark = watermark | ||
| 99 | |||
| 100 | var timers []Timer | ||
| 101 | for t.queue.Len() > 0 { | ||
| 102 | earliestTimer := t.queue[0] | ||
| 103 | |||
| 104 | if earliestTimer.Timestamp > watermark { | ||
| 105 | break | ||
| 106 | } | ||
| 107 | |||
| 108 | pop := heap.Pop(&t.queue).(Timer) | ||
| 109 | timers = append(timers, pop) | ||
| 110 | } | ||
| 111 | |||
| 112 | return timers | ||
| 113 | } | ||
diff --git a/core/window.go b/core/window.go new file mode 100644 index 0000000..6c4b98f --- /dev/null +++ b/core/window.go | |||
| @@ -0,0 +1,54 @@ | |||
| 1 | package core | ||
| 2 | |||
| 3 | import "fmt" | ||
| 4 | |||
| 5 | type Window interface { | ||
| 6 | // MaxTimestamp is the maximum time the Window is Open | ||
| 7 | MaxTimestamp() int64 | ||
| 8 | // ToString convert Window to string format | ||
| 9 | ToString() string | ||
| 10 | } | ||
| 11 | |||
| 12 | type TimeWindow struct { | ||
| 13 | Start int64 | ||
| 14 | End int64 | ||
| 15 | } | ||
| 16 | |||
| 17 | func (t TimeWindow) MaxTimestamp() int64 { | ||
| 18 | // MaxTimestamp is 1ms before the End | ||
| 19 | return t.End - 1 | ||
| 20 | } | ||
| 21 | |||
| 22 | func (t TimeWindow) ToString() string { | ||
| 23 | return fmt.Sprintf("timewindow(%d,%d)", t.Start, t.End) | ||
| 24 | } | ||
| 25 | |||
| 26 | type WindowAssigner[IN any] interface { | ||
| 27 | AssignWindows(element IN, timestamp int64) []Window | ||
| 28 | } | ||
| 29 | |||
| 30 | type TumblingWindows[IN any] struct { | ||
| 31 | Size int64 | ||
| 32 | } | ||
| 33 | |||
| 34 | func NewTumblingWindows[IN any](size int64) *TumblingWindows[IN] { | ||
| 35 | return &TumblingWindows[IN]{ | ||
| 36 | Size: size, | ||
| 37 | } | ||
| 38 | } | ||
| 39 | |||
| 40 | func (w *TumblingWindows[IN]) AssignWindows(element IN, timestamp int64) []Window { | ||
| 41 | // module to get start end | ||
| 42 | // if timestamp = 1200, size 1000 | ||
| 43 | // so start will 1200 - 200 = 1000, end will 1000 + 1000 = 2000 | ||
| 44 | start := timestamp - (timestamp % w.Size) | ||
| 45 | |||
| 46 | end := start + w.Size | ||
| 47 | |||
| 48 | return []Window{ | ||
| 49 | TimeWindow{ | ||
| 50 | Start: start, | ||
| 51 | End: end, | ||
| 52 | }, | ||
| 53 | } | ||
| 54 | } | ||
| @@ -0,0 +1,3 @@ | |||
| 1 | module flux | ||
| 2 | |||
| 3 | go 1.26.3 | ||
| @@ -0,0 +1,96 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "fmt" | ||
| 6 | "strconv" | ||
| 7 | "sync" | ||
| 8 | |||
| 9 | "flux/core" | ||
| 10 | "flux/core/operator" | ||
| 11 | "flux/runtime" | ||
| 12 | "flux/state" | ||
| 13 | "flux/transport" | ||
| 14 | ) | ||
| 15 | |||
| 16 | type Transaction struct { | ||
| 17 | User string | ||
| 18 | Amount int | ||
| 19 | Timestamp int64 | ||
| 20 | } | ||
| 21 | |||
| 22 | type TransactionSummary[IN Transaction, OUT Transaction] struct { | ||
| 23 | totalTransaction int | ||
| 24 | totalAmount int | ||
| 25 | average int | ||
| 26 | } | ||
| 27 | |||
| 28 | func (t *TransactionSummary[IN, OUT]) Apply(ctx context.Context, key string, window core.Window, in []Transaction, out transport.Emitter[Transaction]) error { | ||
| 29 | for _, v := range in { | ||
| 30 | t.totalAmount += v.Amount | ||
| 31 | out.Emit(ctx, core.StreamRecord[Transaction]{ | ||
| 32 | Value: v, | ||
| 33 | Type: core.RecordData, | ||
| 34 | Timestamp: v.Timestamp, | ||
| 35 | }) | ||
| 36 | } | ||
| 37 | |||
| 38 | return nil | ||
| 39 | } | ||
| 40 | |||
| 41 | func main() { | ||
| 42 | // init | ||
| 43 | ctx := context.Background() | ||
| 44 | |||
| 45 | backend := state.NewMemoryStateBackend(1) | ||
| 46 | |||
| 47 | ctx = core.InjectStateBackend(ctx, backend) | ||
| 48 | |||
| 49 | txList := []Transaction{ | ||
| 50 | { | ||
| 51 | User: "Sang", | ||
| 52 | Amount: 1, | ||
| 53 | Timestamp: 1000, | ||
| 54 | }, | ||
| 55 | { | ||
| 56 | User: "Sang", | ||
| 57 | Amount: 4, | ||
| 58 | Timestamp: 1200, | ||
| 59 | }, | ||
| 60 | { | ||
| 61 | User: "Sang", | ||
| 62 | Amount: 10, | ||
| 63 | Timestamp: 1600, | ||
| 64 | }, | ||
| 65 | } | ||
| 66 | |||
| 67 | ch1 := make(chan core.StreamRecord[Transaction], len(txList)) | ||
| 68 | ch2 := make(chan core.StreamRecord[Transaction], len(txList)) | ||
| 69 | |||
| 70 | wo := operator.NewWatermarkGeneratorOperator[Transaction](0, func(in Transaction) int64 { | ||
| 71 | return in.Timestamp | ||
| 72 | }) | ||
| 73 | |||
| 74 | var wg sync.WaitGroup | ||
| 75 | |||
| 76 | runtime.RunTask[Transaction, Transaction](ctx, transport.NewLocalReceiveChannel(ch1), transport.NewLocalEmitChannel(ch2), wo) | ||
| 77 | |||
| 78 | wg.Go(func() { | ||
| 79 | for i, tx := range txList { | ||
| 80 | ch1 <- core.NewDataRecord(strconv.Itoa(i), tx, tx.Timestamp) | ||
| 81 | } | ||
| 82 | |||
| 83 | close(ch1) | ||
| 84 | }) | ||
| 85 | |||
| 86 | wg.Go(func() { | ||
| 87 | for v := range ch2 { | ||
| 88 | if v.Type == core.RecordData { | ||
| 89 | fmt.Println("data: ", v.Value) | ||
| 90 | } else { | ||
| 91 | fmt.Println("watermark: ", v.Timestamp) | ||
| 92 | } | ||
| 93 | } | ||
| 94 | }) | ||
| 95 | wg.Wait() | ||
| 96 | } | ||
diff --git a/runtime/task.go b/runtime/task.go new file mode 100644 index 0000000..734976e --- /dev/null +++ b/runtime/task.go | |||
| @@ -0,0 +1,66 @@ | |||
| 1 | package runtime | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "fmt" | ||
| 6 | "time" | ||
| 7 | |||
| 8 | "flux/core/operator" | ||
| 9 | "flux/transport" | ||
| 10 | ) | ||
| 11 | |||
| 12 | // RunTask runs a stream task in the background. The returned channel yields the | ||
| 13 | // first error that stopped it (context.Canceled when ctx ends early) and is | ||
| 14 | // closed when the task is done, so callers can join on it. | ||
| 15 | func RunTask[IN, OUT any](ctx context.Context, input transport.Receiver[IN], output transport.Emitter[OUT], op operator.Operator[IN, OUT]) <-chan error { | ||
| 16 | done := make(chan error, 1) | ||
| 17 | |||
| 18 | go func() { | ||
| 19 | defer close(done) | ||
| 20 | if err := runTask(ctx, input, output, op); err != nil { | ||
| 21 | done <- err | ||
| 22 | } | ||
| 23 | }() | ||
| 24 | |||
| 25 | return done | ||
| 26 | } | ||
| 27 | |||
| 28 | func runTask[IN, OUT any](ctx context.Context, input transport.Receiver[IN], output transport.Emitter[OUT], op operator.Operator[IN, OUT]) (err error) { | ||
| 29 | // ponytail: assumes one writer per output channel. Fan-in needs a refcount | ||
| 30 | // or a dedicated closer, otherwise the second Close panics. | ||
| 31 | defer func() { | ||
| 32 | if cerr := output.Close(); cerr != nil && err == nil { | ||
| 33 | err = fmt.Errorf("close output: %w", cerr) | ||
| 34 | } | ||
| 35 | }() | ||
| 36 | |||
| 37 | if err := op.Open(ctx); err != nil { | ||
| 38 | return fmt.Errorf("open operator: %w", err) | ||
| 39 | } | ||
| 40 | defer func() { | ||
| 41 | // ctx is usually already cancelled by the time we get here — that is | ||
| 42 | // why the task stopped. Close still needs a live window to flush, so | ||
| 43 | // give it one that keeps ctx's values but drops the cancellation. | ||
| 44 | closeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) | ||
| 45 | defer cancel() | ||
| 46 | |||
| 47 | if cerr := op.Close(closeCtx); cerr != nil && err == nil { | ||
| 48 | err = fmt.Errorf("close operator: %w", cerr) | ||
| 49 | } | ||
| 50 | }() | ||
| 51 | |||
| 52 | for { | ||
| 53 | record, ok := input.Receive(ctx) | ||
| 54 | if !ok { | ||
| 55 | break | ||
| 56 | } | ||
| 57 | |||
| 58 | if err := op.Process(ctx, record, output); err != nil { | ||
| 59 | return fmt.Errorf("process record: %w", err) | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | // Receive also returns !ok on cancellation, so report that as an error | ||
| 64 | // rather than a clean drain. | ||
| 65 | return ctx.Err() | ||
| 66 | } | ||
diff --git a/state/memory_backend.go b/state/memory_backend.go new file mode 100644 index 0000000..6594657 --- /dev/null +++ b/state/memory_backend.go | |||
| @@ -0,0 +1,57 @@ | |||
| 1 | package state | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | "flux/core" | ||
| 6 | ) | ||
| 7 | |||
| 8 | // TODO: currently using Golang Map for go through the DAG pipline | ||
| 9 | // This should be use index map index map[uint64]uint32 Hash(KeyGroup + StateName + Key) -> Offset | ||
| 10 | // with 1GB of []byte allocation | ||
| 11 | // This should be optimize with ZERO GC Scanning, Memory Alignment | ||
| 12 | |||
| 13 | type MemoryStateBackend struct { | ||
| 14 | maxParallelism int | ||
| 15 | currentKey string | ||
| 16 | currentGroupKey int | ||
| 17 | currentNamespace any | ||
| 18 | // 4D structure StateName -> KeyGroup -> Key -> Namespace -> Value | ||
| 19 | states map[string]map[int]map[string]map[any]any | ||
| 20 | } | ||
| 21 | |||
| 22 | func NewMemoryStateBackend(maxParallelism int) *MemoryStateBackend { | ||
| 23 | return &MemoryStateBackend{ | ||
| 24 | maxParallelism: maxParallelism, | ||
| 25 | states: make(map[string]map[int]map[string]map[any]any), | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | func (ms *MemoryStateBackend) GetCurrentKey() string { | ||
| 30 | return ms.currentKey | ||
| 31 | } | ||
| 32 | |||
| 33 | func (ms *MemoryStateBackend) SetCurrentKey(key string) error { | ||
| 34 | ms.currentKey = key | ||
| 35 | ms.currentGroupKey = core.AssignToKeyGroup(key, ms.maxParallelism) | ||
| 36 | return nil | ||
| 37 | } | ||
| 38 | |||
| 39 | func (ms *MemoryStateBackend) GetValueState(ctx context.Context, key string) (core.ValueState[any], error) { | ||
| 40 | // Assign if state not existed | ||
| 41 | |||
| 42 | if _, ok := ms.states[key]; !ok { | ||
| 43 | ms.states[key] = make(map[int]map[string]map[any]any) | ||
| 44 | } | ||
| 45 | |||
| 46 | return &memoryStateValue{backend: ms, stateName: key}, nil | ||
| 47 | } | ||
| 48 | |||
| 49 | func (ms *MemoryStateBackend) GetCurrentNamespace() any{ | ||
| 50 | |||
| 51 | return ms.currentNamespace | ||
| 52 | } | ||
| 53 | |||
| 54 | func (ms *MemoryStateBackend) SetCurrentNamespace(namespace any) error { | ||
| 55 | ms.currentNamespace = namespace | ||
| 56 | return nil | ||
| 57 | } | ||
diff --git a/state/memory_state.go b/state/memory_state.go new file mode 100644 index 0000000..95c7318 --- /dev/null +++ b/state/memory_state.go | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | package state | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | ) | ||
| 6 | |||
| 7 | type memoryStateValue struct { | ||
| 8 | backend *MemoryStateBackend | ||
| 9 | stateName string | ||
| 10 | } | ||
| 11 | |||
| 12 | func (m *memoryStateValue) Value(ctx context.Context) (any, error) { | ||
| 13 | kg := m.backend.currentGroupKey | ||
| 14 | key := m.backend.currentKey | ||
| 15 | ns := m.backend.currentNamespace | ||
| 16 | if kgMap, exist := m.backend.states[m.stateName][kg][key]; exist { | ||
| 17 | if v, hasKey := kgMap[ns]; hasKey { | ||
| 18 | return v, nil | ||
| 19 | } | ||
| 20 | } | ||
| 21 | |||
| 22 | return nil, nil | ||
| 23 | } | ||
| 24 | |||
| 25 | func (m *memoryStateValue) Update(ctx context.Context, newVal any) (any, error) { | ||
| 26 | kg := m.backend.currentGroupKey | ||
| 27 | key := m.backend.currentKey | ||
| 28 | ns := m.backend.currentNamespace | ||
| 29 | if _, exists := m.backend.states[m.stateName][kg][key]; !exists { | ||
| 30 | m.backend.states[m.stateName][kg] = make(map[string]map[any]any) | ||
| 31 | } | ||
| 32 | |||
| 33 | m.backend.states[m.stateName][kg][key][ns] = newVal | ||
| 34 | return newVal, nil | ||
| 35 | } | ||
| 36 | |||
| 37 | func (m *memoryStateValue) Clear(ctx context.Context) error { | ||
| 38 | kg := m.backend.currentGroupKey | ||
| 39 | key := m.backend.currentKey | ||
| 40 | if kgMap, exists := m.backend.states[m.stateName][kg]; exists { | ||
| 41 | delete(kgMap, key) | ||
| 42 | } | ||
| 43 | return nil | ||
| 44 | } | ||
diff --git a/transport/channel.go b/transport/channel.go new file mode 100644 index 0000000..56d5f2b --- /dev/null +++ b/transport/channel.go | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | package transport | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | |||
| 6 | "flux/core" | ||
| 7 | ) | ||
| 8 | |||
| 9 | type Receiver[T any] interface { | ||
| 10 | // Receive returns the next record, or ok=false once the stream is done or | ||
| 11 | // ctx is cancelled. | ||
| 12 | Receive(ctx context.Context) (core.StreamRecord[T], bool) | ||
| 13 | Close() error | ||
| 14 | } | ||
| 15 | |||
| 16 | // Emitter mirrors core.Emitter plus Close. It is declared separately because | ||
| 17 | // core cannot import transport without an import cycle. | ||
| 18 | type Emitter[T any] interface { | ||
| 19 | // Emit blocks until the record is handed off, or returns ctx.Err() if ctx | ||
| 20 | // is cancelled first. | ||
| 21 | Emit(ctx context.Context, record core.StreamRecord[T]) error | ||
| 22 | Close() error | ||
| 23 | } | ||
diff --git a/transport/local_channel.go b/transport/local_channel.go new file mode 100644 index 0000000..2fac029 --- /dev/null +++ b/transport/local_channel.go | |||
| @@ -0,0 +1,56 @@ | |||
| 1 | package transport | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "context" | ||
| 5 | |||
| 6 | "flux/core" | ||
| 7 | ) | ||
| 8 | |||
| 9 | type LocalReceiveChannel[T any] struct { | ||
| 10 | recordCh <-chan core.StreamRecord[T] | ||
| 11 | } | ||
| 12 | |||
| 13 | func NewLocalReceiveChannel[T any](ch <-chan core.StreamRecord[T]) *LocalReceiveChannel[T] { | ||
| 14 | return &LocalReceiveChannel[T]{ | ||
| 15 | recordCh: ch, | ||
| 16 | } | ||
| 17 | } | ||
| 18 | |||
| 19 | func (c *LocalReceiveChannel[T]) Receive(ctx context.Context) (core.StreamRecord[T], bool) { | ||
| 20 | select { | ||
| 21 | case val, ok := <-c.recordCh: | ||
| 22 | return val, ok | ||
| 23 | case <-ctx.Done(): | ||
| 24 | var zero core.StreamRecord[T] | ||
| 25 | return zero, false | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | func (c *LocalReceiveChannel[T]) Close() error { | ||
| 30 | // The emitter will close | ||
| 31 | return nil | ||
| 32 | } | ||
| 33 | |||
| 34 | type LocalEmitChannel[T any] struct { | ||
| 35 | recordCh chan<- core.StreamRecord[T] | ||
| 36 | } | ||
| 37 | |||
| 38 | func NewLocalEmitChannel[T any](ch chan<- core.StreamRecord[T]) *LocalEmitChannel[T] { | ||
| 39 | return &LocalEmitChannel[T]{ | ||
| 40 | recordCh: ch, | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | func (l *LocalEmitChannel[T]) Emit(ctx context.Context, c core.StreamRecord[T]) error { | ||
| 45 | select { | ||
| 46 | case l.recordCh <- c: | ||
| 47 | return nil | ||
| 48 | case <-ctx.Done(): | ||
| 49 | return ctx.Err() | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | func (l *LocalEmitChannel[T]) Close() error { | ||
| 54 | close(l.recordCh) | ||
| 55 | return nil | ||
| 56 | } | ||
