diff options
Diffstat (limited to 'core/operator')
| -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 |
8 files changed, 464 insertions, 0 deletions
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 | } | ||
