aboutsummaryrefslogtreecommitdiff
path: root/core/operator
diff options
context:
space:
mode:
authorsangtran <tranquangsang12.7@gmail.com>2026-08-29 01:08:28 +0700
committersangtran <tranquangsang12.7@gmail.com>2026-08-29 01:08:28 +0700
commit96c07bab1086510a55a004e9a1b90c30bc723a5c (patch)
tree7abbc615c51667e111fdef7b2ef69a9fc663f2ca /core/operator
signatureVerified sangtran <tranquangsang12.7@gmail.com> Key: 8DD79823D235E1BB ultimate trust
downloadflux-96c07bab1086510a55a004e9a1b90c30bc723a5c.tar.gz
flux-96c07bab1086510a55a004e9a1b90c30bc723a5c.zip
Verified feat: init base draft support window
Diffstat (limited to 'core/operator')
-rw-r--r--core/operator/filter.go36
-rw-r--r--core/operator/keyby.go32
-rw-r--r--core/operator/keyed_process.go81
-rw-r--r--core/operator/map.go53
-rw-r--r--core/operator/operator.go16
-rw-r--r--core/operator/stateful_map.go75
-rw-r--r--core/operator/watermark.go61
-rw-r--r--core/operator/window.go110
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 @@
1package operator
2
3import (
4 "context"
5 "flux/core"
6)
7
8type FilterFunc[IN any] func(in IN) bool
9
10type FilterOperator[IN any] struct {
11 userFunc FilterFunc[IN]
12}
13
14func NewFilterOperator[IN any](userFunc FilterFunc[IN]) *FilterOperator[IN] {
15 return &FilterOperator[IN]{userFunc: userFunc}
16}
17
18func (f *FilterOperator[IN]) Open(ctx context.Context) error {
19 return nil
20}
21
22func (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
34func (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 @@
1package operator
2
3import (
4 "context"
5 "flux/core"
6)
7
8type KeySelector[IN any] func(in IN) string
9type KeyByOperator[IN any] struct {
10 selector KeySelector[IN]
11}
12
13func NewKeyByOperator[IN any](selector KeySelector[IN]) *KeyByOperator[IN] {
14 return &KeyByOperator[IN]{selector: selector}
15}
16
17func (k *KeyByOperator[IN]) Open(ctx context.Context) error {
18 return nil
19}
20
21func (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
30func (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 @@
1package operator
2
3import (
4 "context"
5 "flux/core"
6 "fmt"
7)
8
9type 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
16type KeyProcessOperator[IN, OUT any] struct {
17 userFunc KeyedProcesser[IN, OUT]
18 timeService core.InternalTimerService
19 backend core.StateBackend
20}
21
22func 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
29func (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
43func (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
79func (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 @@
1package operator
2
3import (
4 "context"
5 "flux/core"
6 "fmt"
7)
8
9// MapFunc is business logic for user implement
10type MapFunc[IN, OUT any] func(in IN) (OUT, error)
11
12type MapOperator[IN, OUT any] struct {
13 userFunc MapFunc[IN, OUT]
14}
15
16func NewMapOperator[IN, OUT any](fn MapFunc[IN, OUT]) *MapOperator[IN, OUT] {
17 return &MapOperator[IN, OUT]{
18 userFunc: fn,
19 }
20}
21
22func (fo *MapOperator[IN, OUT]) Open(ctx context.Context) error {
23 return nil
24}
25
26func (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
51func (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 @@
1package operator
2
3import (
4 "context"
5 "flux/core"
6)
7
8type Emitter[OUT any] interface {
9 Emit(ctx context.Context, record core.StreamRecord[OUT]) error
10}
11
12type 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 @@
1package operator
2
3import (
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.
13type 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
19type StatefulMapOperator[IN, OUT any] struct {
20 userFunc RichMapFunc[IN, OUT]
21 backend core.StateBackend
22}
23
24func NewStatefulMapOperator[IN, OUT any](fn RichMapFunc[IN, OUT]) *StatefulMapOperator[IN, OUT] {
25 return &StatefulMapOperator[IN, OUT]{userFunc: fn}
26}
27
28func (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
40func (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
73func (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 @@
1package operator
2
3import (
4 "context"
5
6 "flux/core"
7)
8
9// TimestampAssigner is responsible for event time defined by user
10type TimestampAssigner[IN any] func(in IN) int64
11
12type WaterGeneratorOperator[IN any] struct {
13 assigner TimestampAssigner[IN]
14 maxOutOfOrder int64
15 currentMaxTimestamp int64
16 lastEmittedWatermark int64
17}
18
19func 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
27func (w *WaterGeneratorOperator[IN]) Open(ctx context.Context) error {
28 return nil
29}
30
31func (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
59func (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 @@
1package operator
2
3import (
4 "context"
5 "fmt"
6
7 "flux/core"
8)
9
10// WindowFunction is for user interface implements the behaviour
11type WindowFunction[IN, OUT any] interface {
12 Apply(ctx context.Context, key string, window core.Window, in []IN, out Emitter[OUT]) error
13}
14
15type 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
24func 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
32func (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
48func (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
108func (o *WindowOperator[IN, OUT]) Close(ctx context.Context) error {
109 return nil
110}