aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README.md14
-rw-r--r--core/hash.go19
-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
-rw-r--r--core/record.go40
-rw-r--r--core/state.go10
-rw-r--r--core/state_backend.go62
-rw-r--r--core/timer.go113
-rw-r--r--core/window.go54
-rw-r--r--go.mod3
-rw-r--r--main.go96
-rw-r--r--runtime/task.go66
-rw-r--r--state/memory_backend.go57
-rw-r--r--state/memory_state.go44
-rw-r--r--transport/channel.go23
-rw-r--r--transport/local_channel.go56
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
13Should 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 @@
1package core
2
3import "hash/fnv"
4
5const DefaultMaxParallelism = 128
6
7func 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 @@
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}
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 @@
1package core
2
3type RecordType byte
4
5const (
6 RecordData RecordType = iota
7 RecordWatermark
8 RecordBarrier
9)
10
11type 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
19func 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
28func NewWatermarkRecord[T any](timestamp int64) StreamRecord[T] {
29 return StreamRecord[T]{
30 Type: RecordWatermark,
31 Timestamp: timestamp,
32 }
33}
34
35func 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 @@
1package core
2
3import "context"
4
5// ValueState is for client side using to update state
6type 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 @@
1package core
2
3import (
4 "context"
5)
6
7type 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
21type stateBackendKeyType struct{}
22
23var stateBackendKey stateBackendKeyType
24
25type typedValueState[T any] struct {
26 inner ValueState[any]
27}
28
29func CastValueType[T any](inner ValueState[any]) ValueState[T] {
30 return &typedValueState[T]{inner: inner}
31}
32
33func (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
46func (s *typedValueState[T]) Update(ctx context.Context, newVal T) (T, error) {
47 _, err := s.inner.Update(ctx, newVal)
48 return newVal, err
49}
50
51func (s *typedValueState[T]) Clear(ctx context.Context) error {
52 return s.inner.Clear(ctx)
53}
54
55func InjectStateBackend(ctx context.Context, state StateBackend) context.Context {
56 return context.WithValue(ctx, stateBackendKey, state)
57}
58
59func 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 @@
1package core
2
3import (
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
12type timerServiceKey struct{}
13
14// InjectTimerService inject the timer into Context
15func 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
20func ExtractTimerService(ctx context.Context) (TimerService, bool) {
21 ts, ok := ctx.Value(timerServiceKey{}).(TimerService)
22 return ts, ok
23}
24
25type 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
38type timerHeap []Timer
39
40func (h *timerHeap) Len() int { return len(*h) }
41func (h *timerHeap) Less(i, j int) bool { return (*h)[i].Timestamp < (*h)[j].Timestamp }
42func (h *timerHeap) Swap(i, j int) {
43 (*h)[i], (*h)[j] = (*h)[j], (*h)[i]
44}
45
46func (h *timerHeap) Push(x any) {
47 *h = append(*h, x.(Timer))
48}
49
50func (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
58type TimerService interface {
59 Register(time int64, key string, keyspace any)
60 CurrentWatermark() int64
61}
62
63type InternalTimerService interface {
64 TimerService
65 AdvanceWatermark(watermark int64) []Timer
66}
67
68type timerService struct {
69 queue timerHeap
70 currentWatermark int64
71}
72
73func NewTimerService() InternalTimerService {
74 return &timerService{
75 queue: make(timerHeap, 0),
76 currentWatermark: -1,
77 }
78}
79
80func (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
88func (t *timerService) CurrentWatermark() int64 {
89 return t.currentWatermark
90}
91
92func (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 @@
1package core
2
3import "fmt"
4
5type 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
12type TimeWindow struct {
13 Start int64
14 End int64
15}
16
17func (t TimeWindow) MaxTimestamp() int64 {
18 // MaxTimestamp is 1ms before the End
19 return t.End - 1
20}
21
22func (t TimeWindow) ToString() string {
23 return fmt.Sprintf("timewindow(%d,%d)", t.Start, t.End)
24}
25
26type WindowAssigner[IN any] interface {
27 AssignWindows(element IN, timestamp int64) []Window
28}
29
30type TumblingWindows[IN any] struct {
31 Size int64
32}
33
34func NewTumblingWindows[IN any](size int64) *TumblingWindows[IN] {
35 return &TumblingWindows[IN]{
36 Size: size,
37 }
38}
39
40func (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}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1094a64
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
1module flux
2
3go 1.26.3
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..1b4a4be
--- /dev/null
+++ b/main.go
@@ -0,0 +1,96 @@
1package main
2
3import (
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
16type Transaction struct {
17 User string
18 Amount int
19 Timestamp int64
20}
21
22type TransactionSummary[IN Transaction, OUT Transaction] struct {
23 totalTransaction int
24 totalAmount int
25 average int
26}
27
28func (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
41func 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 @@
1package runtime
2
3import (
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.
15func 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
28func 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 @@
1package state
2
3import (
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
13type 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
22func 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
29func (ms *MemoryStateBackend) GetCurrentKey() string {
30 return ms.currentKey
31}
32
33func (ms *MemoryStateBackend) SetCurrentKey(key string) error {
34 ms.currentKey = key
35 ms.currentGroupKey = core.AssignToKeyGroup(key, ms.maxParallelism)
36 return nil
37}
38
39func (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
49func (ms *MemoryStateBackend) GetCurrentNamespace() any{
50
51 return ms.currentNamespace
52}
53
54func (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 @@
1package state
2
3import (
4"context"
5)
6
7type memoryStateValue struct {
8 backend *MemoryStateBackend
9 stateName string
10}
11
12func (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
25func (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
37func (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 @@
1package transport
2
3import (
4 "context"
5
6 "flux/core"
7)
8
9type 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.
18type 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 @@
1package transport
2
3import (
4 "context"
5
6 "flux/core"
7)
8
9type LocalReceiveChannel[T any] struct {
10 recordCh <-chan core.StreamRecord[T]
11}
12
13func NewLocalReceiveChannel[T any](ch <-chan core.StreamRecord[T]) *LocalReceiveChannel[T] {
14 return &LocalReceiveChannel[T]{
15 recordCh: ch,
16 }
17}
18
19func (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
29func (c *LocalReceiveChannel[T]) Close() error {
30 // The emitter will close
31 return nil
32}
33
34type LocalEmitChannel[T any] struct {
35 recordCh chan<- core.StreamRecord[T]
36}
37
38func NewLocalEmitChannel[T any](ch chan<- core.StreamRecord[T]) *LocalEmitChannel[T] {
39 return &LocalEmitChannel[T]{
40 recordCh: ch,
41 }
42}
43
44func (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
53func (l *LocalEmitChannel[T]) Close() error {
54 close(l.recordCh)
55 return nil
56}