diff --git a/.testcontainers.properties b/.testcontainers.properties new file mode 100644 index 0000000..2c1834f --- /dev/null +++ b/.testcontainers.properties @@ -0,0 +1 @@ +ryuk.disabled=true \ No newline at end of file diff --git a/Makefile b/Makefile index 0248ac9..4ad5994 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ $(PLATFORMS): run: go run main.go start +generate: + go generate ./.. + build: go build -v . diff --git a/app/discovery/autostop.go b/app/discovery/autostop.go index 01d0a11..1c6d77e 100644 --- a/app/discovery/autostop.go +++ b/app/discovery/autostop.go @@ -2,8 +2,9 @@ package discovery import ( "context" + "errors" "github.com/sablierapp/sablier/app/providers" - "github.com/sablierapp/sablier/pkg/arrays" + "github.com/sablierapp/sablier/pkg/store" log "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" ) @@ -12,7 +13,7 @@ import ( // as running instances by Sablier. // By default, Sablier does not stop all already running instances. Meaning that you need to make an // initial request in order to trigger the scaling to zero. -func StopAllUnregisteredInstances(ctx context.Context, provider providers.Provider, registered []string) error { +func StopAllUnregisteredInstances(ctx context.Context, provider providers.Provider, s store.Store) error { log.Info("Stopping all unregistered running instances") log.Tracef("Retrieving all instances with label [%v=true]", LabelEnable) @@ -25,12 +26,14 @@ func StopAllUnregisteredInstances(ctx context.Context, provider providers.Provid } log.Tracef("Found %v instances with label [%v=true]", len(instances), LabelEnable) - names := make([]string, 0, len(instances)) + unregistered := make([]string, 0) for _, instance := range instances { - names = append(names, instance.Name) + _, err = s.Get(ctx, instance.Name) + if errors.Is(err, store.ErrKeyNotFound) { + unregistered = append(unregistered, instance.Name) + } } - unregistered := arrays.RemoveElements(names, registered) log.Tracef("Found %v unregistered instances ", len(instances)) waitGroup := errgroup.Group{} diff --git a/app/discovery/autostop_test.go b/app/discovery/autostop_test.go index ca2f1a2..f08df06 100644 --- a/app/discovery/autostop_test.go +++ b/app/discovery/autostop_test.go @@ -4,10 +4,14 @@ import ( "context" "errors" "github.com/sablierapp/sablier/app/discovery" + "github.com/sablierapp/sablier/app/instance" "github.com/sablierapp/sablier/app/providers" "github.com/sablierapp/sablier/app/providers/mock" "github.com/sablierapp/sablier/app/types" + "github.com/sablierapp/sablier/pkg/store/inmemory" + "gotest.tools/v3/assert" "testing" + "time" ) func TestStopAllUnregisteredInstances(t *testing.T) { @@ -20,7 +24,9 @@ func TestStopAllUnregisteredInstances(t *testing.T) { {Name: "instance2"}, {Name: "instance3"}, } - registered := []string{"instance1"} + store := inmemory.NewInMemory() + err := store.Put(ctx, instance.State{Name: "instance1"}, time.Minute) + assert.NilError(t, err) // Set up expectations for InstanceList mockProvider.On("InstanceList", ctx, providers.InstanceListOptions{ @@ -33,10 +39,8 @@ func TestStopAllUnregisteredInstances(t *testing.T) { mockProvider.On("Stop", ctx, "instance3").Return(nil) // Call the function under test - err := discovery.StopAllUnregisteredInstances(ctx, mockProvider, registered) - if err != nil { - t.Fatalf("Expected no error, but got %v", err) - } + err = discovery.StopAllUnregisteredInstances(ctx, mockProvider, store) + assert.NilError(t, err) // Check expectations mockProvider.AssertExpectations(t) @@ -52,7 +56,9 @@ func TestStopAllUnregisteredInstances_WithError(t *testing.T) { {Name: "instance2"}, {Name: "instance3"}, } - registered := []string{"instance1"} + store := inmemory.NewInMemory() + err := store.Put(ctx, instance.State{Name: "instance1"}, time.Minute) + assert.NilError(t, err) // Set up expectations for InstanceList mockProvider.On("InstanceList", ctx, providers.InstanceListOptions{ @@ -65,10 +71,8 @@ func TestStopAllUnregisteredInstances_WithError(t *testing.T) { mockProvider.On("Stop", ctx, "instance3").Return(nil) // Call the function under test - err := discovery.StopAllUnregisteredInstances(ctx, mockProvider, registered) - if err == nil { - t.Fatalf("Expected error, but got nil") - } + err = discovery.StopAllUnregisteredInstances(ctx, mockProvider, store) + assert.Error(t, err, "stop error") // Check expectations mockProvider.AssertExpectations(t) diff --git a/app/sablier.go b/app/sablier.go index 35c57d6..68abd18 100644 --- a/app/sablier.go +++ b/app/sablier.go @@ -8,23 +8,27 @@ import ( "github.com/sablierapp/sablier/app/providers/docker" "github.com/sablierapp/sablier/app/providers/dockerswarm" "github.com/sablierapp/sablier/app/providers/kubernetes" + "github.com/sablierapp/sablier/pkg/store/inmemory" "log/slog" "os" + "os/signal" + "syscall" + "time" - "github.com/sablierapp/sablier/app/instance" "github.com/sablierapp/sablier/app/providers" "github.com/sablierapp/sablier/app/sessions" "github.com/sablierapp/sablier/app/storage" "github.com/sablierapp/sablier/app/theme" "github.com/sablierapp/sablier/config" "github.com/sablierapp/sablier/internal/server" - "github.com/sablierapp/sablier/pkg/tinykv" "github.com/sablierapp/sablier/version" log "github.com/sirupsen/logrus" ) func Start(ctx context.Context, conf config.Config) error { - + // Create context that listens for the interrupt signal from the OS. + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + defer stop() logLevel, err := log.ParseLevel(conf.Logging.Level) if err != nil { @@ -45,7 +49,11 @@ func Start(ctx context.Context, conf config.Config) error { log.Infof("using provider \"%s\"", conf.Provider.Name) - store := tinykv.New(conf.Sessions.ExpirationInterval, onSessionExpires(provider)) + store := inmemory.NewInMemory() + err = store.OnExpire(ctx, onSessionExpires(provider)) + if err != nil { + return err + } storage, err := storage.NewFileStorage(conf.Storage) if err != nil { @@ -55,13 +63,39 @@ func Start(ctx context.Context, conf config.Config) error { sessionsManager := sessions.NewSessionsManager(store, provider) defer sessionsManager.Stop() + groups, err := provider.GetGroups(ctx) + if err != nil { + log.Warn("could not get groups", err) + } else { + sessionsManager.SetGroups(groups) + } + + updateGroups := make(chan map[string][]string) + go WatchGroups(ctx, provider, 2*time.Second, updateGroups) + go func() { + for groups := range updateGroups { + sessionsManager.SetGroups(groups) + } + }() + + instanceStopped := make(chan string) + go provider.NotifyInstanceStopped(ctx, instanceStopped) + go func() { + for stopped := range instanceStopped { + err := sessionsManager.RemoveInstance(stopped) + if err != nil { + logger.Warn("could not remove instance", slog.Any("error", err)) + } + } + }() + if storage.Enabled() { defer saveSessions(storage, sessionsManager) loadSessions(storage, sessionsManager) } if conf.Provider.AutoStopOnStartup { - err := discovery.StopAllUnregisteredInstances(context.Background(), provider, store.Keys()) + err := discovery.StopAllUnregisteredInstances(context.Background(), provider, store) if err != nil { log.Warnf("Stopping unregistered instances had an error: %v", err) } @@ -91,14 +125,28 @@ func Start(ctx context.Context, conf config.Config) error { SessionsConfig: conf.Sessions, } - server.Start(ctx, logger, conf.Server, strategy) + go server.Start(ctx, logger, conf.Server, strategy) + + // Listen for the interrupt signal. + <-ctx.Done() + + // Restore default behavior on the interrupt signal and notify user of shutdown. + stop() + log.Println("shutting down gracefully, press Ctrl+C again to force") + + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + log.Println("Server exiting") return nil } -func onSessionExpires(provider providers.Provider) func(key string, instance instance.State) { - return func(_key string, _instance instance.State) { - go func(key string, instance instance.State) { +func onSessionExpires(provider providers.Provider) func(key string) { + return func(_key string) { + go func(key string) { log.Debugf("stopping %s...", key) err := provider.Stop(context.Background(), key) @@ -107,11 +155,12 @@ func onSessionExpires(provider providers.Provider) func(key string, instance ins } else { log.Debugf("stopped %s", key) } - }(_key, _instance) + }(_key) } } func loadSessions(storage storage.Storage, sessions sessions.Manager) { + slog.Info("loading sessions from storage") reader, err := storage.Reader() if err != nil { log.Error("error loading sessions", err) @@ -123,6 +172,7 @@ func loadSessions(storage storage.Storage, sessions sessions.Manager) { } func saveSessions(storage storage.Storage, sessions sessions.Manager) { + slog.Info("writing sessions to storage") writer, err := storage.Writer() if err != nil { log.Error("error saving sessions", err) @@ -149,3 +199,20 @@ func NewProvider(config config.Provider) (providers.Provider, error) { } return nil, fmt.Errorf("unimplemented provider %s", config.Name) } + +func WatchGroups(ctx context.Context, provider providers.Provider, frequency time.Duration, send chan<- map[string][]string) { + ticker := time.NewTicker(frequency) + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + groups, err := provider.GetGroups(ctx) + if err != nil { + log.Warn("could not get groups", err) + } else if groups != nil { + send <- groups + } + } + } +} diff --git a/app/sessions/groups_watcher.go b/app/sessions/groups_watcher.go deleted file mode 100644 index 619e23e..0000000 --- a/app/sessions/groups_watcher.go +++ /dev/null @@ -1,27 +0,0 @@ -package sessions - -import ( - "context" - "time" - - "github.com/sablierapp/sablier/app/providers" - log "github.com/sirupsen/logrus" -) - -// watchGroups watches indefinitely for new groups -func watchGroups(ctx context.Context, provider providers.Provider, frequency time.Duration, send chan<- map[string][]string) { - ticker := time.NewTicker(frequency) - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - groups, err := provider.GetGroups(ctx) - if err != nil { - log.Warn("could not get groups", err) - } else { - send <- groups - } - } - } -} diff --git a/app/sessions/sessions_manager.go b/app/sessions/sessions_manager.go index a9ccb52..59426f8 100644 --- a/app/sessions/sessions_manager.go +++ b/app/sessions/sessions_manager.go @@ -5,7 +5,9 @@ import ( "encoding/json" "errors" "fmt" + "github.com/sablierapp/sablier/pkg/store" "io" + "log/slog" "maps" "slices" "sync" @@ -13,12 +15,9 @@ import ( "github.com/sablierapp/sablier/app/instance" "github.com/sablierapp/sablier/app/providers" - "github.com/sablierapp/sablier/pkg/tinykv" log "github.com/sirupsen/logrus" ) -const defaultRefreshFrequency = 2 * time.Second - //go:generate mockgen -package sessionstest -source=sessions_manager.go -destination=sessionstest/mocks_sessions_manager.go * type Manager interface { @@ -30,6 +29,9 @@ type Manager interface { LoadSessions(io.ReadCloser) error SaveSessions(io.WriteCloser) error + RemoveInstance(name string) error + SetGroups(groups map[string][]string) + Stop() } @@ -37,71 +39,57 @@ type SessionsManager struct { ctx context.Context cancel context.CancelFunc - store tinykv.KV[instance.State] + store store.Store provider providers.Provider groups map[string][]string } -func NewSessionsManager(store tinykv.KV[instance.State], provider providers.Provider) Manager { +func NewSessionsManager(store store.Store, provider providers.Provider) Manager { ctx, cancel := context.WithCancel(context.Background()) - groups, err := provider.GetGroups(ctx) - if err != nil { - groups = make(map[string][]string) - log.Warn("could not get groups", err) - } - sm := &SessionsManager{ ctx: ctx, cancel: cancel, store: store, provider: provider, - groups: groups, + groups: map[string][]string{}, } - sm.initWatchers() - return sm } -func (sm *SessionsManager) initWatchers() { - updateGroups := make(chan map[string][]string) - go watchGroups(sm.ctx, sm.provider, defaultRefreshFrequency, updateGroups) - go sm.consumeGroups(updateGroups) - - instanceStopped := make(chan string) - go sm.provider.NotifyInstanceStopped(sm.ctx, instanceStopped) - go sm.consumeInstanceStopped(instanceStopped) +func (sm *SessionsManager) SetGroups(groups map[string][]string) { + if groups == nil { + groups = map[string][]string{} + } + slog.Info("set groups", slog.Any("old", sm.groups), slog.Any("new", groups)) + sm.groups = groups } -func (sm *SessionsManager) consumeGroups(receive chan map[string][]string) { - for groups := range receive { - sm.groups = groups - } -} - -func (sm *SessionsManager) consumeInstanceStopped(instanceStopped chan string) { - for instance := range instanceStopped { - // Will delete from the store containers that have been stop either by external sources - // or by the internal expiration loop, if the deleted entry does not exist, it doesn't matter - log.Debugf("received event instance %s is stopped, removing from store", instance) - sm.store.Delete(instance) - } +func (sm *SessionsManager) RemoveInstance(name string) error { + return sm.store.Delete(context.Background(), name) } func (sm *SessionsManager) LoadSessions(reader io.ReadCloser) error { + unmarshaler, ok := sm.store.(json.Unmarshaler) defer reader.Close() - return json.NewDecoder(reader).Decode(sm.store) + if ok { + return json.NewDecoder(reader).Decode(unmarshaler) + } + return nil } func (sm *SessionsManager) SaveSessions(writer io.WriteCloser) error { + marshaler, ok := sm.store.(json.Marshaler) defer writer.Close() + if ok { + encoder := json.NewEncoder(writer) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") - encoder := json.NewEncoder(writer) - encoder.SetEscapeHTML(false) - encoder.SetIndent("", " ") - - return encoder.Encode(sm.store) + return encoder.Encode(marshaler) + } + return nil } type InstanceState struct { @@ -110,26 +98,21 @@ type InstanceState struct { } type SessionState struct { - Instances *sync.Map + Instances map[string]InstanceState `json:"instances"` } func (s *SessionState) IsReady() bool { - ready := true - if s.Instances == nil { - s.Instances = &sync.Map{} + s.Instances = map[string]InstanceState{} } - s.Instances.Range(func(key, value interface{}) bool { - state := value.(InstanceState) - if state.Error != nil || state.Instance.Status != instance.Ready { - ready = false + for _, v := range s.Instances { + if v.Error != nil || v.Instance.Status != instance.Ready { return false } - return true - }) + } - return ready + return true } func (s *SessionState) Status() string { @@ -148,7 +131,7 @@ func (s *SessionsManager) RequestSession(names []string, duration time.Duration) var wg sync.WaitGroup sessionState = &SessionState{ - Instances: &sync.Map{}, + Instances: map[string]InstanceState{}, } wg.Add(len(names)) @@ -158,10 +141,10 @@ func (s *SessionsManager) RequestSession(names []string, duration time.Duration) defer wg.Done() state, err := s.requestSessionInstance(name, duration) - sessionState.Instances.Store(name, InstanceState{ + sessionState.Instances[name] = InstanceState{ Instance: state, Error: err, - }) + } }(names[i]) } @@ -195,9 +178,8 @@ func (s *SessionsManager) requestSessionInstance(name string, duration time.Dura return nil, errors.New("instance name cannot be empty") } - requestState, exists := s.store.Get(name) - - if !exists { + requestState, err := s.store.Get(context.TODO(), name) + if errors.Is(err, store.ErrKeyNotFound) { log.Debugf("starting [%s]...", name) err := s.provider.Start(s.ctx, name) @@ -217,6 +199,8 @@ func (s *SessionsManager) requestSessionInstance(name string, duration time.Dura requestState.Message = state.Message log.Debugf("status for [%s]=[%s]", name, requestState.Status) + } else if err != nil { + return nil, fmt.Errorf("cannot retrieve instance from store: %w", err) } else if requestState.Status != instance.Ready { log.Debugf("checking [%s]...", name) state, err := s.provider.GetState(s.ctx, name) @@ -250,6 +234,7 @@ func (s *SessionsManager) RequestReadySession(ctx context.Context, names []strin ticker := time.NewTicker(5 * time.Second) readiness := make(chan *SessionState) + errch := make(chan error) quit := make(chan struct{}) go func() { @@ -258,6 +243,7 @@ func (s *SessionsManager) RequestReadySession(ctx context.Context, names []strin case <-ticker.C: session, err := s.RequestSession(names, duration) if err != nil { + errch <- err return } if session.IsReady() { @@ -274,10 +260,16 @@ func (s *SessionsManager) RequestReadySession(ctx context.Context, names []strin case <-ctx.Done(): log.Debug("request cancelled by user, stopping timeout") close(quit) + if ctx.Err() != nil { + return nil, fmt.Errorf("request cancelled by user: %w", ctx.Err()) + } return nil, fmt.Errorf("request cancelled by user") case status := <-readiness: close(quit) return status, nil + case err := <-errch: + close(quit) + return nil, err case <-time.After(timeout): close(quit) return nil, fmt.Errorf("session was not ready after %s", timeout.String()) @@ -306,25 +298,19 @@ func (s *SessionsManager) RequestReadySessionGroup(ctx context.Context, group st } func (s *SessionsManager) ExpiresAfter(instance *instance.State, duration time.Duration) { - s.store.Put(instance.Name, *instance, duration) + err := s.store.Put(context.TODO(), *instance, duration) + if err != nil { + slog.Default().Warn("could not put instance to store, will not expire", slog.Any("error", err), slog.String("instance", instance.Name)) + } } func (s *SessionsManager) Stop() { // Stop event listeners s.cancel() - - // Stop the store - s.store.Stop() } func (s *SessionState) MarshalJSON() ([]byte, error) { - instances := []InstanceState{} - - s.Instances.Range(func(key, value interface{}) bool { - state := value.(InstanceState) - instances = append(instances, state) - return true - }) + instances := maps.Values(s.Instances) return json.Marshal(map[string]any{ "instances": instances, diff --git a/app/sessions/sessions_manager_test.go b/app/sessions/sessions_manager_test.go index 8249154..baa26b5 100644 --- a/app/sessions/sessions_manager_test.go +++ b/app/sessions/sessions_manager_test.go @@ -2,7 +2,8 @@ package sessions import ( "context" - "sync" + "github.com/sablierapp/sablier/pkg/store/storetest" + "go.uber.org/mock/gomock" "testing" "time" @@ -14,7 +15,7 @@ import ( func TestSessionState_IsReady(t *testing.T) { type fields struct { - Instances *sync.Map + Instances map[string]InstanceState Error error } tests := []struct { @@ -72,102 +73,73 @@ func TestSessionState_IsReady(t *testing.T) { } } -func createMap(instances []*instance.State) (store *sync.Map) { - store = &sync.Map{} +func createMap(instances []*instance.State) map[string]InstanceState { + states := make(map[string]InstanceState) for _, v := range instances { - store.Store(v.Name, InstanceState{ + states[v.Name] = InstanceState{ Instance: v, Error: nil, - }) + } } - return + return states } -func TestNewSessionsManagerEvents(t *testing.T) { - tests := []struct { - name string - stoppedInstances []string - }{ - { - name: "when nginx is stopped it is removed from the store", - stoppedInstances: []string{"nginx"}, - }, - { - name: "when nginx, apache and whoami is stopped it is removed from the store", - stoppedInstances: []string{"nginx", "apache", "whoami"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - provider := mocks.NewProviderMockWithStoppedInstancesEvents(tt.stoppedInstances) - provider.Add(1) +func setupSessionManager(t *testing.T) (Manager, *storetest.MockStore, *mocks.ProviderMock) { + t.Helper() + ctrl := gomock.NewController(t) - kv := mocks.NewKVMock() - kv.Add(len(tt.stoppedInstances)) - kv.Mock.On("Delete", mock.AnythingOfType("string")).Return() + p := mocks.NewProviderMock() + s := storetest.NewMockStore(ctrl) - NewSessionsManager(kv, provider) + m := NewSessionsManager(s, p) + return m, s, p +} - // The provider watches notifications from a Goroutine, must wait - provider.Wait() - // The key is deleted inside a Goroutine by the session manager, must wait - kv.Wait() - - for _, instance := range tt.stoppedInstances { - kv.AssertCalled(t, "Delete", instance) - } - }) - } +func TestSessionsManager(t *testing.T) { + t.Run("RemoveInstance", func(t *testing.T) { + manager, store, _ := setupSessionManager(t) + store.EXPECT().Delete(gomock.Any(), "test") + err := manager.RemoveInstance("test") + assert.NilError(t, err) + }) } func TestSessionsManager_RequestReadySessionCancelledByUser(t *testing.T) { - t.Run("request ready session is cancelled by user", func(t *testing.T) { - kvmock := mocks.NewKVMock() - kvmock.On("Get", mock.Anything).Return(instance.State{Name: "apache", Status: instance.NotReady}, true) - - providermock := mocks.NewProviderMock() - providermock.On("GetState", mock.Anything).Return(instance.State{Name: "apache", Status: instance.NotReady}, nil) - - s := &SessionsManager{ - store: kvmock, - provider: providermock, - } - ctx, cancel := context.WithCancel(context.Background()) + manager, store, provider := setupSessionManager(t) + store.EXPECT().Get(gomock.Any(), gomock.Any()).Return(instance.State{Name: "apache", Status: instance.NotReady}, nil).AnyTimes() + store.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + + provider.On("GetState", mock.Anything).Return(instance.State{Name: "apache", Status: instance.NotReady}, nil) errchan := make(chan error) go func() { - _, err := s.RequestReadySession(ctx, []string{"nginx", "whoami"}, time.Minute, time.Minute) + _, err := manager.RequestReadySession(ctx, []string{"apache"}, time.Minute, time.Minute) errchan <- err }() // Cancel the call cancel() - assert.Error(t, <-errchan, "request cancelled by user") + assert.Error(t, <-errchan, "request cancelled by user: context canceled") }) } func TestSessionsManager_RequestReadySessionCancelledByTimeout(t *testing.T) { t.Run("request ready session is cancelled by timeout", func(t *testing.T) { - kvmock := mocks.NewKVMock() - kvmock.On("Get", mock.Anything).Return(instance.State{Name: "apache", Status: instance.NotReady}, true) + manager, store, provider := setupSessionManager(t) + store.EXPECT().Get(gomock.Any(), gomock.Any()).Return(instance.State{Name: "apache", Status: instance.NotReady}, nil).AnyTimes() + store.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() - providermock := mocks.NewProviderMock() - providermock.On("GetState", mock.Anything).Return(instance.State{Name: "apache", Status: instance.NotReady}, nil) - - s := &SessionsManager{ - store: kvmock, - provider: providermock, - } + provider.On("GetState", mock.Anything).Return(instance.State{Name: "apache", Status: instance.NotReady}, nil) errchan := make(chan error) go func() { - _, err := s.RequestReadySession(context.Background(), []string{"nginx", "whoami"}, time.Minute, time.Second) + _, err := manager.RequestReadySession(context.Background(), []string{"apache"}, time.Minute, time.Second) errchan <- err }() @@ -178,19 +150,13 @@ func TestSessionsManager_RequestReadySessionCancelledByTimeout(t *testing.T) { func TestSessionsManager_RequestReadySession(t *testing.T) { t.Run("request ready session is ready", func(t *testing.T) { - kvmock := mocks.NewKVMock() - kvmock.On("Get", mock.Anything).Return(instance.State{Name: "apache", Status: instance.Ready}, true) - - providermock := mocks.NewProviderMock() - - s := &SessionsManager{ - store: kvmock, - provider: providermock, - } + manager, store, _ := setupSessionManager(t) + store.EXPECT().Get(gomock.Any(), gomock.Any()).Return(instance.State{Name: "apache", Status: instance.Ready}, nil).AnyTimes() + store.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() errchan := make(chan error) go func() { - _, err := s.RequestReadySession(context.Background(), []string{"nginx", "whoami"}, time.Minute, time.Second) + _, err := manager.RequestReadySession(context.Background(), []string{"apache"}, time.Minute, time.Second) errchan <- err }() diff --git a/app/sessions/sessionstest/mocks_sessions_manager.go b/app/sessions/sessionstest/mocks_sessions_manager.go index 4438c9a..4acd324 100644 --- a/app/sessions/sessionstest/mocks_sessions_manager.go +++ b/app/sessions/sessionstest/mocks_sessions_manager.go @@ -57,6 +57,20 @@ func (mr *MockManagerMockRecorder) LoadSessions(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSessions", reflect.TypeOf((*MockManager)(nil).LoadSessions), arg0) } +// RemoveInstance mocks base method. +func (m *MockManager) RemoveInstance(name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveInstance", name) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveInstance indicates an expected call of RemoveInstance. +func (mr *MockManagerMockRecorder) RemoveInstance(name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveInstance", reflect.TypeOf((*MockManager)(nil).RemoveInstance), name) +} + // RequestReadySession mocks base method. func (m *MockManager) RequestReadySession(ctx context.Context, names []string, duration, timeout time.Duration) (*sessions.SessionState, error) { m.ctrl.T.Helper() @@ -131,6 +145,18 @@ func (mr *MockManagerMockRecorder) SaveSessions(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSessions", reflect.TypeOf((*MockManager)(nil).SaveSessions), arg0) } +// SetGroups mocks base method. +func (m *MockManager) SetGroups(groups map[string][]string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetGroups", groups) +} + +// SetGroups indicates an expected call of SetGroups. +func (mr *MockManagerMockRecorder) SetGroups(groups any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGroups", reflect.TypeOf((*MockManager)(nil).SetGroups), groups) +} + // Stop mocks base method. func (m *MockManager) Stop() { m.ctrl.T.Helper() diff --git a/app/types/session.go b/app/types/session.go deleted file mode 100644 index ab1254f..0000000 --- a/app/types/session.go +++ /dev/null @@ -1 +0,0 @@ -package types diff --git a/go.mod b/go.mod index dbee907..c549d41 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 github.com/tniswong/go.rfcx v0.0.0-20181019234604-07783c52761f + github.com/valkey-io/valkey-go v1.0.53 go.uber.org/mock v0.5.0 golang.org/x/sync v0.10.0 gotest.tools/v3 v3.5.1 @@ -27,17 +28,22 @@ require ( ) require ( - github.com/Microsoft/go-winio v0.6.1 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/bytedance/sonic v1.11.9 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.5.0 // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.14.1 // indirect @@ -49,6 +55,7 @@ require ( github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect @@ -71,9 +78,10 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.2 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -81,24 +89,35 @@ require ( github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/sergi/go-diff v1.0.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/testcontainers/testcontainers-go v0.35.0 // indirect + github.com/testcontainers/testcontainers-go/modules/valkey v0.35.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect @@ -110,7 +129,8 @@ require ( github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect @@ -120,15 +140,15 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/mod v0.18.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/tools v0.23.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index fbd3f62..a83c8d9 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/acouvreur/httpexpect/v2 v2.16.0 h1:FGXaR9jt6IQMXxpqbM8YpX7EEvyERU0Lps3ooEc/gk8= github.com/acouvreur/httpexpect/v2 v2.16.0/go.mod h1:7myOP3A3VyS4+qnA4cm8DAad8zMN+7zxDB80W9f8yIc= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= @@ -20,6 +26,10 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -29,10 +39,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -60,6 +74,9 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -121,6 +138,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -134,6 +153,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI= +github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -149,6 +170,14 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -164,12 +193,14 @@ github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= @@ -179,6 +210,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -192,6 +225,10 @@ github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQ github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -225,12 +262,22 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= +github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= +github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= +github.com/testcontainers/testcontainers-go/modules/valkey v0.35.0 h1:0cX9txu8oW4NVXzaGMLBEOX/BBmWmQtd1X55JILNb6E= +github.com/testcontainers/testcontainers-go/modules/valkey v0.35.0/go.mod h1:Bro7Md5b9MoFzM1bs/NWEwazdePpYBy96thih94pYxs= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/tniswong/go.rfcx v0.0.0-20181019234604-07783c52761f h1:C43EMGXFtvYf/zunHR6ivZV7Z6ytg73t0GXwYyicXMQ= github.com/tniswong/go.rfcx v0.0.0-20181019234604-07783c52761f/go.mod h1:N+sR0vLSCTtI6o06PMWsjMB4TVqqDttKNq4iC9wvxVY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/valkey-io/valkey-go v1.0.53 h1:bntDqQVPzkLdE/4ypXBrHalXJB+BOTMk+JwXNRCGudg= +github.com/valkey-io/valkey-go v1.0.53/go.mod h1:BXlVAPIL9rFQinSFM+N32JfWzfCaUAqBpZkc4vPY6fM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= @@ -255,8 +302,12 @@ github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcm github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= @@ -286,13 +337,13 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -310,14 +361,18 @@ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= @@ -339,8 +394,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/go.work.sum b/go.work.sum index 9ac8a0b..b0576a9 100644 --- a/go.work.sum +++ b/go.work.sum @@ -72,6 +72,8 @@ github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= +github.com/containerd/containerd v1.7.19 h1:/xQ4XRJ0tamDkdzrrBAUy/LE5nCcxFKdBm4EcPrSMEE= +github.com/containerd/containerd v1.7.19/go.mod h1:h4FtNYUUMB4Phr6v+xG89RYKj9XccvbNSCKjdufCrkc= github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= @@ -125,6 +127,7 @@ github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5 github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -288,6 +291,8 @@ github.com/sagikazarmark/crypt v0.19.0/go.mod h1:c6vimRziqqERhtSe0MhIvzE1w54FrCH github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/savsgio/gotils v0.0.0-20210617111740-97865ed5a873 h1:N3Af8f13ooDKcIhsmFT7Z05CStZWu4C7Md0uDEy4q6o= github.com/savsgio/gotils v0.0.0-20210617111740-97865ed5a873/go.mod h1:dmPawKuiAeG/aFYVs2i+Dyosoo7FNcm+Pi8iK6ZUrX8= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= @@ -355,16 +360,15 @@ golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= @@ -373,10 +377,13 @@ golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= @@ -395,18 +402,17 @@ golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= @@ -422,6 +428,8 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240509183442-62759503f434/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/internal/api/start_dynamic.go b/internal/api/start_dynamic.go index a04c54d..b7d8992 100644 --- a/internal/api/start_dynamic.go +++ b/internal/api/start_dynamic.go @@ -96,15 +96,10 @@ func sessionStateToRenderOptionsInstanceState(sessionState *sessions.SessionStat log.Warnf("sessionStateToRenderOptionsInstanceState: sessionState is nil") return } - sessionState.Instances.Range(func(key, value any) bool { - if value != nil { - instances = append(instances, instanceStateToRenderOptionsRequestState(value.(sessions.InstanceState).Instance)) - } else { - log.Warnf("sessionStateToRenderOptionsInstanceState: sessionState instance is nil, key: %v", key) - } - return true - }) + for _, v := range sessionState.Instances { + instances = append(instances, instanceStateToRenderOptionsRequestState(v.Instance)) + } sort.SliceStable(instances, func(i, j int) bool { return strings.Compare(instances[i].Name, instances[j].Name) == -1 diff --git a/internal/api/start_dynamic_test.go b/internal/api/start_dynamic_test.go index 471d398..298613b 100644 --- a/internal/api/start_dynamic_test.go +++ b/internal/api/start_dynamic_test.go @@ -2,6 +2,7 @@ package api import ( "errors" + "github.com/sablierapp/sablier/app/instance" "github.com/sablierapp/sablier/app/sessions" "github.com/tniswong/go.rfcx/rfc7807" "go.uber.org/mock/gomock" @@ -10,6 +11,23 @@ import ( "testing" ) +func session() *sessions.SessionState { + state := instance.ReadyInstanceState("test", 1) + state2 := instance.ReadyInstanceState("test2", 1) + return &sessions.SessionState{ + Instances: map[string]sessions.InstanceState{ + "test": { + Instance: &state, + Error: nil, + }, + "test2": { + Instance: &state2, + Error: nil, + }, + }, + } +} + func TestStartDynamic(t *testing.T) { t.Run("StartDynamicInvalidBind", func(t *testing.T) { app, router, strategy, _ := NewApiTest(t) @@ -35,7 +53,7 @@ func TestStartDynamic(t *testing.T) { t.Run("StartDynamicThemeNotFound", func(t *testing.T) { app, router, strategy, m := NewApiTest(t) StartDynamic(router, strategy) - m.EXPECT().RequestSessionGroup("test", gomock.Any()).Return(&sessions.SessionState{}, nil) + m.EXPECT().RequestSessionGroup("test", gomock.Any()).Return(session(), nil) r := PerformRequest(app, "GET", "/api/strategies/dynamic?group=test&theme=invalid") assert.Equal(t, http.StatusNotFound, r.Code) assert.Equal(t, rfc7807.JSONMediaType, r.Header().Get("Content-Type")) @@ -43,7 +61,7 @@ func TestStartDynamic(t *testing.T) { t.Run("StartDynamicByNames", func(t *testing.T) { app, router, strategy, m := NewApiTest(t) StartDynamic(router, strategy) - m.EXPECT().RequestSession([]string{"test"}, gomock.Any()).Return(&sessions.SessionState{}, nil) + m.EXPECT().RequestSession([]string{"test"}, gomock.Any()).Return(session(), nil) r := PerformRequest(app, "GET", "/api/strategies/dynamic?names=test") assert.Equal(t, http.StatusOK, r.Code) assert.Equal(t, SablierStatusReady, r.Header().Get(SablierStatusHeader)) @@ -51,7 +69,7 @@ func TestStartDynamic(t *testing.T) { t.Run("StartDynamicByGroup", func(t *testing.T) { app, router, strategy, m := NewApiTest(t) StartDynamic(router, strategy) - m.EXPECT().RequestSessionGroup("test", gomock.Any()).Return(&sessions.SessionState{}, nil) + m.EXPECT().RequestSessionGroup("test", gomock.Any()).Return(session(), nil) r := PerformRequest(app, "GET", "/api/strategies/dynamic?group=test") assert.Equal(t, http.StatusOK, r.Code) assert.Equal(t, SablierStatusReady, r.Header().Get(SablierStatusHeader)) diff --git a/pkg/store/inmemory/inmemory.go b/pkg/store/inmemory/inmemory.go new file mode 100644 index 0000000..509fa6e --- /dev/null +++ b/pkg/store/inmemory/inmemory.go @@ -0,0 +1,56 @@ +package inmemory + +import ( + "context" + "encoding/json" + "github.com/sablierapp/sablier/app/instance" + "github.com/sablierapp/sablier/pkg/store" + "github.com/sablierapp/sablier/pkg/tinykv" + "time" +) + +var _ store.Store = (*InMemory)(nil) +var _ json.Marshaler = (*InMemory)(nil) +var _ json.Unmarshaler = (*InMemory)(nil) + +func NewInMemory() store.Store { + return &InMemory{ + kv: tinykv.New[instance.State](1*time.Second, nil), + } +} + +type InMemory struct { + kv tinykv.KV[instance.State] +} + +func (i InMemory) UnmarshalJSON(bytes []byte) error { + return i.kv.UnmarshalJSON(bytes) +} + +func (i InMemory) MarshalJSON() ([]byte, error) { + return i.kv.MarshalJSON() +} + +func (i InMemory) Get(_ context.Context, s string) (instance.State, error) { + val, ok := i.kv.Get(s) + if !ok { + return instance.State{}, store.ErrKeyNotFound + } + return val, nil +} + +func (i InMemory) Put(_ context.Context, state instance.State, duration time.Duration) error { + return i.kv.Put(state.Name, state, duration) +} + +func (i InMemory) Delete(_ context.Context, s string) error { + i.kv.Delete(s) + return nil +} + +func (i InMemory) OnExpire(_ context.Context, f func(string)) error { + i.kv.SetOnExpire(func(k string, _ instance.State) { + f(k) + }) + return nil +} diff --git a/pkg/store/inmemory/inmemory_test.go b/pkg/store/inmemory/inmemory_test.go new file mode 100644 index 0000000..245ce26 --- /dev/null +++ b/pkg/store/inmemory/inmemory_test.go @@ -0,0 +1,74 @@ +package inmemory + +import ( + "context" + "github.com/sablierapp/sablier/app/instance" + "github.com/sablierapp/sablier/pkg/store" + "gotest.tools/v3/assert" + "testing" + "time" +) + +func TestInMemory(t *testing.T) { + t.Parallel() + t.Run("InMemoryErrNotFound", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + vk := NewInMemory() + + _, err := vk.Get(ctx, "test") + assert.ErrorIs(t, err, store.ErrKeyNotFound) + }) + t.Run("InMemoryPut", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + vk := NewInMemory() + + err := vk.Put(ctx, instance.State{Name: "test"}, 1*time.Second) + assert.NilError(t, err) + + i, err := vk.Get(ctx, "test") + assert.NilError(t, err) + assert.Equal(t, i.Name, "test") + + <-time.After(2 * time.Second) + _, err = vk.Get(ctx, "test") + assert.ErrorIs(t, err, store.ErrKeyNotFound) + }) + t.Run("InMemoryDelete", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + vk := NewInMemory() + + err := vk.Put(ctx, instance.State{Name: "test"}, 30*time.Second) + assert.NilError(t, err) + + i, err := vk.Get(ctx, "test") + assert.NilError(t, err) + assert.Equal(t, i.Name, "test") + + err = vk.Delete(ctx, "test") + assert.NilError(t, err) + + _, err = vk.Get(ctx, "test") + assert.ErrorIs(t, err, store.ErrKeyNotFound) + }) + t.Run("InMemoryOnExpire", func(t *testing.T) { + t.Parallel() + vk := NewInMemory() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + expirations := make(chan string) + err := vk.OnExpire(ctx, func(key string) { + expirations <- key + }) + assert.NilError(t, err) + + err = vk.Put(ctx, instance.State{Name: "test"}, 1*time.Second) + assert.NilError(t, err) + expired := <-expirations + assert.Equal(t, expired, "test") + }) +} diff --git a/pkg/store/store.go b/pkg/store/store.go new file mode 100644 index 0000000..8453321 --- /dev/null +++ b/pkg/store/store.go @@ -0,0 +1,19 @@ +package store + +import ( + "context" + "errors" + "github.com/sablierapp/sablier/app/instance" + "time" +) + +var ErrKeyNotFound = errors.New("key not found") + +//go:generate mockgen -package storetest -source=store.go -destination=storetest/mocks_store.go * + +type Store interface { + Get(context.Context, string) (instance.State, error) + Put(context.Context, instance.State, time.Duration) error + Delete(context.Context, string) error + OnExpire(context.Context, func(string)) error +} diff --git a/pkg/store/storetest/mocks_store.go b/pkg/store/storetest/mocks_store.go new file mode 100644 index 0000000..ece093b --- /dev/null +++ b/pkg/store/storetest/mocks_store.go @@ -0,0 +1,100 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: store.go +// +// Generated by this command: +// +// mockgen -package storetest -source=store.go -destination=storetest/mocks_store.go * +// + +// Package storetest is a generated GoMock package. +package storetest + +import ( + context "context" + reflect "reflect" + time "time" + + instance "github.com/sablierapp/sablier/app/instance" + gomock "go.uber.org/mock/gomock" +) + +// MockStore is a mock of Store interface. +type MockStore struct { + ctrl *gomock.Controller + recorder *MockStoreMockRecorder + isgomock struct{} +} + +// MockStoreMockRecorder is the mock recorder for MockStore. +type MockStoreMockRecorder struct { + mock *MockStore +} + +// NewMockStore creates a new mock instance. +func NewMockStore(ctrl *gomock.Controller) *MockStore { + mock := &MockStore{ctrl: ctrl} + mock.recorder = &MockStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStore) EXPECT() *MockStoreMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockStore) Delete(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockStoreMockRecorder) Delete(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockStore)(nil).Delete), arg0, arg1) +} + +// Get mocks base method. +func (m *MockStore) Get(arg0 context.Context, arg1 string) (instance.State, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret0, _ := ret[0].(instance.State) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockStoreMockRecorder) Get(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStore)(nil).Get), arg0, arg1) +} + +// OnExpire mocks base method. +func (m *MockStore) OnExpire(arg0 context.Context, arg1 func(string)) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OnExpire", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// OnExpire indicates an expected call of OnExpire. +func (mr *MockStoreMockRecorder) OnExpire(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnExpire", reflect.TypeOf((*MockStore)(nil).OnExpire), arg0, arg1) +} + +// Put mocks base method. +func (m *MockStore) Put(arg0 context.Context, arg1 instance.State, arg2 time.Duration) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Put", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// Put indicates an expected call of Put. +func (mr *MockStoreMockRecorder) Put(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockStore)(nil).Put), arg0, arg1, arg2) +} diff --git a/pkg/store/valkey/valkey.go b/pkg/store/valkey/valkey.go new file mode 100644 index 0000000..1e2684b --- /dev/null +++ b/pkg/store/valkey/valkey.go @@ -0,0 +1,81 @@ +package valkey + +import ( + "context" + "encoding/json" + "github.com/sablierapp/sablier/app/instance" + "github.com/sablierapp/sablier/pkg/store" + "github.com/valkey-io/valkey-go" + "log/slog" + "strings" + "time" +) + +var _ store.Store = (*ValKey)(nil) + +type ValKey struct { + Client valkey.Client +} + +func New(ctx context.Context, client valkey.Client) (store.Store, error) { + err := client.Do(ctx, client.B().Ping().Build()).Error() + if err != nil { + return nil, err + } + + err = client.Do(ctx, client.B().ConfigSet().ParameterValue(). + ParameterValue("notify-keyspace-events", "KEx"). + Build()).Error() + if err != nil { + return nil, err + } + + return &ValKey{Client: client}, nil +} + +func (v *ValKey) Get(ctx context.Context, s string) (instance.State, error) { + b, err := v.Client.Do(ctx, v.Client.B().Get().Key(s).Build()).AsBytes() + if valkey.IsValkeyNil(err) { + return instance.State{}, store.ErrKeyNotFound + } + if err != nil { + return instance.State{}, err + } + + var i instance.State + err = json.Unmarshal(b, &i) + if err != nil { + return instance.State{}, err + } + + return i, nil +} + +func (v *ValKey) Put(ctx context.Context, state instance.State, duration time.Duration) error { + value, err := json.Marshal(state) + if err != nil { + return err + } + + return v.Client.Do(ctx, v.Client.B().Set().Key(state.Name).Value(string(value)).Ex(duration).Build()).Error() +} + +func (v *ValKey) Delete(ctx context.Context, s string) error { + return v.Client.Do(ctx, v.Client.B().Del().Key(s).Build()).Error() +} + +func (v *ValKey) OnExpire(ctx context.Context, f func(string)) error { + go func() { + err := v.Client.Receive(ctx, v.Client.B().Psubscribe().Pattern("__key*__:*").Build(), func(msg valkey.PubSubMessage) { + if msg.Message == "expired" { + split := strings.Split(msg.Channel, ":") + key := split[len(split)-1] + f(key) + } + }) + if err != nil { + slog.Error("error subscribing", slog.Any("error", err)) + } + }() + return nil +} diff --git a/pkg/store/valkey/valkey_test.go b/pkg/store/valkey/valkey_test.go new file mode 100644 index 0000000..ff86925 --- /dev/null +++ b/pkg/store/valkey/valkey_test.go @@ -0,0 +1,108 @@ +package valkey + +import ( + "context" + "github.com/sablierapp/sablier/app/instance" + "github.com/sablierapp/sablier/pkg/store" + "github.com/testcontainers/testcontainers-go" + tcvalkey "github.com/testcontainers/testcontainers-go/modules/valkey" + "github.com/testcontainers/testcontainers-go/wait" + "github.com/valkey-io/valkey-go" + "gotest.tools/v3/assert" + "testing" + "time" +) + +func setupValKeyContainer(t *testing.T) valkey.Client { + t.Helper() + ctx := context.Background() + c, err := tcvalkey.Run(ctx, "valkey/valkey:7.2.5", + tcvalkey.WithLogLevel(tcvalkey.LogLevelDebug), + testcontainers.WithWaitStrategy(wait.ForListeningPort("6379/tcp")), + ) + testcontainers.CleanupContainer(t, c) + assert.NilError(t, err) + + uri, err := c.ConnectionString(ctx) + assert.NilError(t, err) + + options, err := valkey.ParseURL(uri) + assert.NilError(t, err) + + client, err := valkey.NewClient(options) + assert.NilError(t, err) + + return client +} + +func setupValKey(t *testing.T) *ValKey { + t.Helper() + client := setupValKeyContainer(t) + vk, err := New(context.Background(), client) + assert.NilError(t, err) + return vk.(*ValKey) +} + +func TestValKey(t *testing.T) { + t.Parallel() + t.Run("ValKeyErrNotFound", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + vk := setupValKey(t) + + _, err := vk.Get(ctx, "test") + assert.ErrorIs(t, err, store.ErrKeyNotFound) + }) + t.Run("ValKeyPut", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + vk := setupValKey(t) + + err := vk.Put(ctx, instance.State{Name: "test"}, 1*time.Second) + assert.NilError(t, err) + + i, err := vk.Get(ctx, "test") + assert.NilError(t, err) + assert.Equal(t, i.Name, "test") + + <-time.After(2 * time.Second) + _, err = vk.Get(ctx, "test") + assert.ErrorIs(t, err, store.ErrKeyNotFound) + }) + t.Run("ValKeyDelete", func(t *testing.T) { + t.Parallel() + ctx := context.Background() + vk := setupValKey(t) + + err := vk.Put(ctx, instance.State{Name: "test"}, 30*time.Second) + assert.NilError(t, err) + + i, err := vk.Get(ctx, "test") + assert.NilError(t, err) + assert.Equal(t, i.Name, "test") + + err = vk.Delete(ctx, "test") + assert.NilError(t, err) + + _, err = vk.Get(ctx, "test") + assert.ErrorIs(t, err, store.ErrKeyNotFound) + }) + t.Run("ValKeyOnExpire", func(t *testing.T) { + t.Parallel() + vk := setupValKey(t) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + expirations := make(chan string) + err := vk.OnExpire(ctx, func(key string) { + expirations <- key + }) + assert.NilError(t, err) + + err = vk.Put(ctx, instance.State{Name: "test"}, 1*time.Second) + assert.NilError(t, err) + expired := <-expirations + assert.Equal(t, expired, "test") + }) +} diff --git a/pkg/tinykv/tinykv.go b/pkg/tinykv/tinykv.go index cd4ff99..11aa441 100644 --- a/pkg/tinykv/tinykv.go +++ b/pkg/tinykv/tinykv.go @@ -64,6 +64,7 @@ type KV[T any] interface { Entries() (entries map[string]entry[T]) Put(k string, v T, expiresAfter time.Duration) error Stop() + SetOnExpire(onExpire func(k string, v T)) MarshalJSON() ([]byte, error) UnmarshalJSON(b []byte) error } @@ -83,23 +84,26 @@ type store[T any] struct { } // New creates a new *store, onExpire is for notification (must be fast). -func New[T any](expirationInterval time.Duration, onExpire ...func(k string, v T)) KV[T] { +func New[T any](expirationInterval time.Duration, onExpire func(k string, v T)) KV[T] { if expirationInterval <= 0 { expirationInterval = time.Second * 20 } res := &store[T]{ + onExpire: onExpire, stop: make(chan struct{}), kv: make(map[string]*entry[T]), expirationInterval: expirationInterval, heap: th{}, } - if len(onExpire) > 0 && onExpire[0] != nil { - res.onExpire = onExpire[0] - } + go res.expireLoop() return res } +func (kv *store[T]) SetOnExpire(onExpire func(k string, v T)) { + kv.onExpire = onExpire +} + // Stop stops the goroutine func (kv *store[T]) Stop() { kv.stopOnce.Do(func() { close(kv.stop) }) diff --git a/pkg/tinykv/tinykv_test.go b/pkg/tinykv/tinykv_test.go index daccb2d..3b0a28b 100644 --- a/pkg/tinykv/tinykv_test.go +++ b/pkg/tinykv/tinykv_test.go @@ -41,7 +41,7 @@ var _ KV[int] = &store[int]{} func TestGetPut(t *testing.T) { assert := assert.New(t) - rg := New[int](0) + rg := New[int](0, nil) defer rg.Stop() rg.Put("1", 1, time.Minute*50) @@ -62,7 +62,7 @@ func TestGetPut(t *testing.T) { func TestKeys(t *testing.T) { assert := assert.New(t) - rg := New[int](0) + rg := New[int](0, nil) defer rg.Stop() rg.Put("1", 1, time.Minute*50) @@ -76,7 +76,7 @@ func TestKeys(t *testing.T) { func TestValues(t *testing.T) { assert := assert.New(t) - rg := New[int](0) + rg := New[int](0, nil) defer rg.Stop() rg.Put("1", 1, time.Minute*50) @@ -90,7 +90,7 @@ func TestValues(t *testing.T) { func TestEntries(t *testing.T) { assert := assert.New(t) - rg := New[int](0) + rg := New[int](0, nil) defer rg.Stop() rg.Put("1", 1, time.Minute*50) @@ -107,7 +107,7 @@ func TestEntries(t *testing.T) { func TestMarshalJSON(t *testing.T) { os.Setenv("TZ", "") assert := assert.New(t) - rg := New[int](0) + rg := New[int](0, nil) defer rg.Stop() rg.Put("3", 3, time.Minute*50) @@ -125,7 +125,7 @@ func TestUnmarshalJSON(t *testing.T) { assert.Nil(err) jsons := `{"1":{"value":1},"2":{"value":2},"3":{"value":3,"expiresAt":` + string(in5MinutesJson) + `}}` - rg := New[int](0) + rg := New[int](0, nil) defer rg.Stop() err = json.Unmarshal([]byte(jsons), &rg) @@ -141,7 +141,7 @@ func TestUnmarshalJSONExpired(t *testing.T) { assert.Nil(err) jsons := `{"1":{"value":1},"2":{"value":2},"3":{"value":3,"expiresAt":` + string(since5MinutesJson) + `}}` - rg := New[int](0) + rg := New[int](0, nil) defer rg.Stop() err = json.Unmarshal([]byte(jsons), &rg) @@ -371,14 +371,14 @@ func TestOrdering(t *testing.T) { } func BenchmarkGetNoValue(b *testing.B) { - rg := New[interface{}](-1) + rg := New[interface{}](-1, nil) for n := 0; n < b.N; n++ { rg.Get("1") } } func BenchmarkGetValue(b *testing.B) { - rg := New[interface{}](-1) + rg := New[interface{}](-1, nil) rg.Put("1", 1, time.Minute*50) for n := 0; n < b.N; n++ { rg.Get("1") @@ -386,7 +386,7 @@ func BenchmarkGetValue(b *testing.B) { } func BenchmarkGetSlidingTimeout(b *testing.B) { - rg := New[interface{}](-1) + rg := New[interface{}](-1, nil) rg.Put("1", 1, time.Second*10) for n := 0; n < b.N; n++ { rg.Get("1") @@ -394,7 +394,7 @@ func BenchmarkGetSlidingTimeout(b *testing.B) { } func BenchmarkPutExpire(b *testing.B) { - rg := New[interface{}](-1) + rg := New[interface{}](-1, nil) for n := 0; n < b.N; n++ { rg.Put("1", 1, time.Second*10) }