Repository navigation
feat: implement real-time cluster synchronization for Pipes via NATS KV watcher #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
aa7fc3e
49faf2a
48ddf2f
c0921e7
8ff5058
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,14 @@ | ||
| package pipes | ||
|
|
||
| import ( | ||
| "context" | ||
| "os" | ||
| "path/filepath" | ||
| "encoding/json" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/nats-io/nats.go/jetstream" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
@@ -266,3 +269,131 @@ | |
| require.NoError(t, os.Mkdir(filepath.Join(dir, "sub"), 0o750)) | ||
| assert.NoError(t, store.loadFromDirectory(ctx, dir)) | ||
| } | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
| // Watcher Tests & NATS Mocks | ||
| // ----------------------------------------------------------------------------- | ||
|
|
||
| // mockWatcher implements jetstream.KeyWatcher for testing | ||
| type mockWatcher struct { | ||
| updates chan jetstream.KeyValueEntry | ||
| } | ||
|
|
||
| func (m *mockWatcher) Updates() <-chan jetstream.KeyValueEntry { | ||
| return m.updates | ||
|
|
||
| } | ||
| func (m *mockWatcher) Stop() error { | ||
| return nil | ||
| } | ||
|
|
||
| // mockKVEntry implements jetstream.KeyValueEntry for testing | ||
| type mockKVEntry struct { | ||
| key string | ||
| val []byte | ||
| op jetstream.KeyValueOp | ||
| rev uint64 // Keeps track of any updates to a pipe (version number) | ||
| delta uint64 // Keeps track of how many more updates are waiting in the queue behind the current one | ||
| } | ||
|
|
||
| func (m *mockKVEntry) Bucket() string { | ||
| return kvBucket | ||
| } | ||
|
|
||
| func (m *mockKVEntry) Key() string { | ||
| return m.key | ||
| } | ||
|
|
||
| func (m *mockKVEntry) Value() []byte { | ||
| return m.val | ||
| } | ||
|
|
||
| func (m *mockKVEntry) Revision() uint64 { | ||
| return m.rev | ||
| } | ||
|
|
||
| func (m *mockKVEntry) Created() time.Time { | ||
| return time.Now() | ||
| } | ||
|
|
||
| func (m *mockKVEntry) Delta() uint64 { | ||
| return m.delta | ||
| } | ||
|
|
||
| func (m *mockKVEntry) Operation() jetstream.KeyValueOp { | ||
| return m.op | ||
| } | ||
|
|
||
| // mockKV implements a subset of jetstream.KeyValue for testing | ||
| type mockKV struct { | ||
| jetstream.KeyValue // embed to satisfy interface for unused methods | ||
| watcher *mockWatcher | ||
| } | ||
|
|
||
| func (m *mockKV) WatchAll(ctx context.Context, opts ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) { | ||
| return m.watcher, nil | ||
| } | ||
|
|
||
| func TestStore_Watch_SyncsCluster(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| // Setup the mock NATS watcher | ||
| updatesCh := make(chan jetstream.KeyValueEntry) | ||
| watcher := &mockWatcher{updates: updatesCh} | ||
| kv := &mockKV{watcher: watcher} | ||
|
|
||
| // Create a Store and inject our mock NATS KV | ||
| store := NewMemoryStore() | ||
| store.kv = kv // Override the nil kv with our mock | ||
|
|
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| // Start the watcher in the background | ||
| go store.Watch(ctx) | ||
|
|
||
| // Yield briefly to let the background goroutine start | ||
| time.Sleep(10 * time.Millisecond) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SHOULD] Avoid using References
|
||
|
|
||
| // Simulate NATS sending a "Put" (A new Pipe was created) | ||
| newPipe := NamedQuery{Name: "test_pipe", SQL: "SELECT 1"} | ||
| data, err := json.Marshal(newPipe) | ||
| require.NoError(t, err) | ||
|
|
||
| updatesCh <- &mockKVEntry{ | ||
| key: "test_pipe", | ||
| val: data, | ||
| op: jetstream.KeyValuePut, | ||
| } | ||
|
|
||
| // Wait for the background goroutine to process the channel | ||
| time.Sleep(10 * time.Millisecond) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // Verify the pipe is now in local memory | ||
| cached := store.Get("test_pipe") | ||
| require.NotNil(t, cached, "Pipe should have been added to cache by watcher") | ||
| assert.Equal(t, "SELECT 1", cached.SQL) | ||
|
|
||
| // Simulate NATS sending a "Delete" (A Pipe was removed) | ||
| updatesCh <- &mockKVEntry{ | ||
| key: "test_pipe", | ||
| op: jetstream.KeyValueDelete, | ||
| } | ||
|
|
||
| time.Sleep(10 * time.Millisecond) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // Verify the pipe was removed from local memory | ||
| assert.Nil(t, store.Get("test_pipe"), "Pipe should have been removed from cache by watcher") | ||
| } | ||
|
|
||
| func TestStore_Watch_NilKV(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| // If the Store is purely in-memory (no NATS configured), Watch should exit cleanly. | ||
| store := NewMemoryStore() | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| // This would panic if the guard clause `if s.kv == nil` wasn't there | ||
| store.Watch(ctx) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[SHOULD] The write lock is held during the entire
switchblock, including thejson.Unmarshaloperation. This blocks all readers (Get,List) and other writers while expensive CPU work (JSON parsing) is performed. It is better to unmarshal the data outside the lock and only acquire the lock for the map update.