From d3a81a2d571ad2f8ab14236e66ff19fcddd43cdf Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Thu, 15 Jul 2021 22:07:30 -0400 Subject: [PATCH] Major fixes and improvements --- config/config.go | 4 +- config/config_test.go | 49 ++++- controller/controller.go | 10 +- main.go | 1 + storage/config.go | 8 +- storage/storage.go | 43 +++-- storage/storage_test.go | 86 +++++++-- storage/store/database/database.go | 30 ++- storage/store/database/database_test.go | 236 ++++++++++++++---------- storage/store/memory/memory.go | 5 + storage/store/memory/util.go | 3 + storage/store/memory/util_test.go | 48 +++-- storage/store/store.go | 4 + storage/type.go | 9 + 14 files changed, 378 insertions(+), 158 deletions(-) create mode 100644 storage/type.go diff --git a/config/config.go b/config/config.go index 58f59606..0d8759d5 100644 --- a/config/config.go +++ b/config/config.go @@ -176,7 +176,9 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) { func validateStorageConfig(config *Config) error { if config.Storage == nil { - config.Storage = &storage.Config{} + config.Storage = &storage.Config{ + Type: storage.TypeInMemory, + } } err := storage.Initialize(config.Storage) if err != nil { diff --git a/config/config_test.go b/config/config_test.go index 65112a4f..c9cb1d31 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1028,6 +1028,49 @@ services: } } +func TestParseAndValidateConfigBytesWithInvalidServiceName(t *testing.T) { + _, err := parseAndValidateConfigBytes([]byte(` +services: + - name: "" + url: https://twinnation.org/health + conditions: + - "[STATUS] == 200" +`)) + if err != core.ErrServiceWithNoName { + t.Error("should've returned an error") + } +} + +func TestParseAndValidateConfigBytesWithInvalidStorageConfig(t *testing.T) { + _, err := parseAndValidateConfigBytes([]byte(` +storage: + type: sqlite +services: + - name: example + url: https://example.org + conditions: + - "[STATUS] == 200" +`)) + if err == nil { + t.Error("should've returned an error, because a file must be specified for a storage of type sqlite") + } +} + +func TestParseAndValidateConfigBytesWithInvalidYAML(t *testing.T) { + _, err := parseAndValidateConfigBytes([]byte(` +storage: + invalid yaml +services: + - name: example + url: https://example.org + conditions: + - "[STATUS] == 200" +`)) + if err == nil { + t.Error("should've returned an error") + } +} + func TestParseAndValidateConfigBytesWithInvalidSecurityConfig(t *testing.T) { _, err := parseAndValidateConfigBytes([]byte(` security: @@ -1041,7 +1084,7 @@ services: - "[STATUS] == 200" `)) if err == nil { - t.Error("Function should've returned an error") + t.Error("should've returned an error") } } @@ -1173,7 +1216,7 @@ kubernetes: target-path: "/health" `)) if err == nil { - t.Error("Function should've returned an error because providing a service-template is mandatory") + t.Error("should've returned an error because providing a service-template is mandatory") } } @@ -1192,7 +1235,7 @@ kubernetes: target-path: "/health" `)) if err == nil { - t.Error("Function should've returned an error because testing with ClusterModeIn isn't supported") + t.Error("should've returned an error because testing with ClusterModeIn isn't supported") } } diff --git a/controller/controller.go b/controller/controller.go index 209b747c..dcfdff4a 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -39,11 +39,11 @@ var ( server *http.Server ) -func init() { - if err := cache.StartJanitor(); err != nil { - log.Fatal("[controller][init] Failed to start cache janitor:", err.Error()) - } -} +//func init() { XXX: Don't think there's any value in using the janitor since the cache max size is this small +// if err := cache.StartJanitor(); err != nil { +// log.Fatal("[controller][init] Failed to start cache janitor:", err.Error()) +// } +//} // Handle creates the router and starts the server func Handle(securityConfig *security.Config, webConfig *config.WebConfig, enableMetrics bool) { diff --git a/main.go b/main.go index c867fb7b..daca0014 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,7 @@ func start(cfg *config.Config) { func stop() { watchdog.Shutdown() + storage.Get().Close() controller.Shutdown() } diff --git a/storage/config.go b/storage/config.go index b20a7528..37ef8687 100644 --- a/storage/config.go +++ b/storage/config.go @@ -1,8 +1,12 @@ package storage -// Config is the configuration for alerting providers +// Config is the configuration for storage type Config struct { // File is the path of the file to use for persistence - // If blank, persistence is disabled. + // If blank, persistence is disabled File string `yaml:"file"` + + // Type of store + // If blank, uses the default in-memory store + Type Type `yaml:"type"` } diff --git a/storage/storage.go b/storage/storage.go index 3890b8fd..3c375a65 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -6,6 +6,7 @@ import ( "time" "github.com/TwinProduction/gatus/storage/store" + "github.com/TwinProduction/gatus/storage/store/database" "github.com/TwinProduction/gatus/storage/store/memory" ) @@ -38,36 +39,52 @@ func Initialize(cfg *Config) error { initialized = true var err error if cancelFunc != nil { - // Stop the active autoSave task + // Stop the active autoSaveStore task, if there's already one cancelFunc() } - if cfg == nil || len(cfg.File) == 0 { - log.Println("[storage][Initialize] Creating storage provider") - provider, _ = memory.NewStore("") + if cfg == nil { + cfg = &Config{} + } + if len(cfg.File) == 0 { + log.Printf("[storage][Initialize] Creating storage provider with type=%s", cfg.Type) } else { - ctx, cancelFunc = context.WithCancel(context.Background()) - log.Printf("[storage][Initialize] Creating storage provider with file=%s", cfg.File) - provider, err = memory.NewStore(cfg.File) + log.Printf("[storage][Initialize] Creating storage provider with type=%s and file=%s", cfg.Type, cfg.File) + } + ctx, cancelFunc = context.WithCancel(context.Background()) + switch cfg.Type { + case TypeSQLite: + provider, err = database.NewStore(string(cfg.Type), cfg.File) if err != nil { return err } - go autoSave(7*time.Minute, ctx) + case TypeInMemory: + fallthrough + default: + if len(cfg.File) > 0 { + provider, err = memory.NewStore(cfg.File) + if err != nil { + return err + } + go autoSaveStore(ctx, provider, 7*time.Minute) + } else { + provider, _ = memory.NewStore("") + } } return nil } -// autoSave automatically calls the SaveFunc function of the provider at every interval -func autoSave(interval time.Duration, ctx context.Context) { +// autoSaveStore automatically calls the Save function of the provider at every interval +func autoSaveStore(ctx context.Context, provider store.Store, interval time.Duration) { for { select { case <-ctx.Done(): - log.Printf("[storage][autoSave] Stopping active job") + log.Printf("[storage][autoSaveStore] Stopping active job") return case <-time.After(interval): - log.Printf("[storage][autoSave] Saving") + log.Printf("[storage][autoSaveStore] Saving") err := provider.Save() if err != nil { - log.Println("[storage][autoSave] Save failed:", err.Error()) + log.Println("[storage][autoSaveStore] Save failed:", err.Error()) } } } diff --git a/storage/storage_test.go b/storage/storage_test.go index 92359cb7..b973c085 100644 --- a/storage/storage_test.go +++ b/storage/storage_test.go @@ -1,37 +1,89 @@ package storage import ( + "fmt" "testing" "time" + + "github.com/TwinProduction/gatus/storage/store/database" ) func TestInitialize(t *testing.T) { - file := t.TempDir() + "/test.db" - err := Initialize(&Config{File: file}) - if err != nil { - t.Fatal("shouldn't have returned an error") + type Scenario struct { + Name string + Cfg *Config + ExpectedErr error } - if cancelFunc == nil { - t.Error("cancelFunc shouldn't have been nil") + scenarios := []Scenario{ + { + Name: "nil", + Cfg: nil, + ExpectedErr: nil, + }, + { + Name: "blank", + Cfg: &Config{}, + ExpectedErr: nil, + }, + { + Name: "inmemory-no-file", + Cfg: &Config{Type: TypeInMemory}, + ExpectedErr: nil, + }, + { + Name: "inmemory-with-file", + Cfg: &Config{Type: TypeInMemory, File: t.TempDir() + "/TestInitialize_inmemory-with-file.db"}, + ExpectedErr: nil, + }, + { + Name: "sqlite-no-file", + Cfg: &Config{Type: TypeSQLite}, + ExpectedErr: database.ErrFilePathNotSpecified, + }, + { + Name: "sqlite-with-file", + Cfg: &Config{Type: TypeSQLite, File: t.TempDir() + "/TestInitialize_sqlite-with-file.db"}, + ExpectedErr: nil, + }, } - if ctx == nil { - t.Error("ctx shouldn't have been nil") + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + err := Initialize(scenario.Cfg) + if err != scenario.ExpectedErr { + t.Errorf("expected %v, got %v", scenario.ExpectedErr, err) + } + if err != nil { + return + } + if cancelFunc == nil { + t.Error("cancelFunc shouldn't have been nil") + } + if ctx == nil { + t.Error("ctx shouldn't have been nil") + } + if provider == nil { + fmt.Println("wtf?") + } + provider.Close() + // Try to initialize it again + err = Initialize(scenario.Cfg) + if err != scenario.ExpectedErr { + t.Errorf("expected %v, got %v", scenario.ExpectedErr, err) + return + } + provider.Close() + provider = nil + }) } - // Try to initialize it again - err = Initialize(&Config{File: file}) - if err != nil { - t.Fatal("shouldn't have returned an error") - } - cancelFunc() } func TestAutoSave(t *testing.T) { - file := t.TempDir() + "/test.db" + file := t.TempDir() + "/TestAutoSave.db" if err := Initialize(&Config{File: file}); err != nil { t.Fatal("shouldn't have returned an error") } - go autoSave(3*time.Millisecond, ctx) + go autoSaveStore(ctx, provider, 3*time.Millisecond) time.Sleep(15 * time.Millisecond) cancelFunc() - time.Sleep(5 * time.Millisecond) + time.Sleep(10 * time.Millisecond) } diff --git a/storage/store/database/database.go b/storage/store/database/database.go index 8e67227f..02b2fce9 100644 --- a/storage/store/database/database.go +++ b/storage/store/database/database.go @@ -3,6 +3,7 @@ package database import ( "database/sql" "errors" + "fmt" "log" "strings" "time" @@ -306,7 +307,18 @@ func (s *Store) Insert(service *core.Service, result *core.Result) { // DeleteAllServiceStatusesNotInKeys removes all rows owned by a service whose key is not within the keys provided func (s *Store) DeleteAllServiceStatusesNotInKeys(keys []string) int { - panic("implement me") + if len(keys) == 0 { + return 0 + } + args := make([]interface{}, 0, len(keys)) + for i := range keys { + args = append(args, keys[i]) + } + _, err := s.db.Exec(fmt.Sprintf("DELETE FROM service WHERE service_key NOT IN (%s)", strings.Trim(strings.Repeat("?,", len(keys)), ",")), args...) + if err != nil { + log.Printf("err: %v", err) + } + return 0 } // Clear deletes everything from the store @@ -439,7 +451,7 @@ func (s *Store) getAllServiceKeys(tx *sql.Tx) (keys []string, err error) { } func (s *Store) getServiceStatusByKey(tx *sql.Tx, key string, parameters *paging.ServiceStatusParams) (*core.ServiceStatus, error) { - serviceID, serviceName, serviceGroup, err := s.getServiceIDGroupAndNameByKey(tx, key) + serviceID, serviceGroup, serviceName, err := s.getServiceIDGroupAndNameByKey(tx, key) if err != nil { return nil, err } @@ -484,7 +496,7 @@ func (s *Store) getEventsByServiceID(tx *sql.Tx, serviceID int64, page, pageSize SELECT event_type, event_timestamp FROM service_event WHERE service_id = $1 - ORDER BY service_event_id DESC + ORDER BY service_event_id ASC LIMIT $2 OFFSET $3 `, serviceID, @@ -509,7 +521,7 @@ func (s *Store) getResultsByServiceID(tx *sql.Tx, serviceID int64, page, pageSiz SELECT service_result_id, success, errors, connected, status, dns_rcode, certificate_expiration, hostname, ip, duration, timestamp FROM service_result WHERE service_id = $1 - ORDER BY timestamp DESC + ORDER BY timestamp ASC LIMIT $2 OFFSET $3 `, serviceID, @@ -525,7 +537,9 @@ func (s *Store) getResultsByServiceID(tx *sql.Tx, serviceID int64, page, pageSiz var id int64 var joinedErrors string _ = rows.Scan(&id, &result.Success, &joinedErrors, &result.Connected, &result.HTTPStatus, &result.DNSRCode, &result.CertificateExpiration, &result.Hostname, &result.IP, &result.Duration, &result.Timestamp) - result.Errors = strings.Split(joinedErrors, arraySeparator) + if len(joinedErrors) != 0 { + result.Errors = strings.Split(joinedErrors, arraySeparator) + } results = append(results, result) idResultMap[id] = result } @@ -534,7 +548,7 @@ func (s *Store) getResultsByServiceID(tx *sql.Tx, serviceID int64, page, pageSiz for serviceResultID, result := range idResultMap { rows, err = tx.Query( ` - SELECT service_result_id, condition, success + SELECT condition, success FROM service_result_condition WHERE service_result_id = $1 `, @@ -545,7 +559,9 @@ func (s *Store) getResultsByServiceID(tx *sql.Tx, serviceID int64, page, pageSiz } for rows.Next() { conditionResult := &core.ConditionResult{} - _ = rows.Scan(&conditionResult.Condition, &conditionResult.Success) + if err = rows.Scan(&conditionResult.Condition, &conditionResult.Success); err != nil { + return + } result.ConditionResults = append(result.ConditionResults, conditionResult) } _ = rows.Close() diff --git a/storage/store/database/database_test.go b/storage/store/database/database_test.go index 89c2163f..583e5cba 100644 --- a/storage/store/database/database_test.go +++ b/storage/store/database/database_test.go @@ -97,7 +97,7 @@ func TestNewStore(t *testing.T) { func TestStore_Insert(t *testing.T) { store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_Insert.db") - defer store.db.Close() + defer store.Close() store.Insert(&testService, &testSuccessfulResult) store.Insert(&testService, &testUnsuccessfulResult) @@ -147,102 +147,9 @@ func TestStore_Insert(t *testing.T) { } } -func TestStore_GetServiceStatus(t *testing.T) { - store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatus.db") - defer store.db.Close() - store.Insert(&testService, &testSuccessfulResult) - store.Insert(&testService, &testUnsuccessfulResult) - - serviceStatus := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) - if serviceStatus == nil { - t.Fatalf("serviceStatus shouldn't have been nil") - } - if serviceStatus.Uptime == nil { - t.Fatalf("serviceStatus.Uptime shouldn't have been nil") - } - if serviceStatus.Uptime.LastHour != 0.5 { - t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5") - } - if serviceStatus.Uptime.LastTwentyFourHours != 0.5 { - t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5") - } - if serviceStatus.Uptime.LastSevenDays != 0.5 { - t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5") - } -} - -func TestStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { - store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatusForMissingStatusReturnsNil.db") - defer store.db.Close() - store.Insert(&testService, &testSuccessfulResult) - - serviceStatus := store.GetServiceStatus("nonexistantgroup", "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) - if serviceStatus != nil { - t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, testService.Name) - } - serviceStatus = store.GetServiceStatus(testService.Group, "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) - if serviceStatus != nil { - t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, "nonexistantname") - } - serviceStatus = store.GetServiceStatus("nonexistantgroup", testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) - if serviceStatus != nil { - t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", "nonexistantgroup", testService.Name) - } -} - -func TestStore_GetServiceStatusByKey(t *testing.T) { - store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatusByKey.db") - defer store.db.Close() - store.Insert(&testService, &testSuccessfulResult) - store.Insert(&testService, &testUnsuccessfulResult) - - serviceStatus := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) - if serviceStatus == nil { - t.Fatalf("serviceStatus shouldn't have been nil") - } - if serviceStatus.Uptime == nil { - t.Fatalf("serviceStatus.Uptime shouldn't have been nil") - } - if serviceStatus.Uptime.LastHour != 0.5 { - t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5") - } - if serviceStatus.Uptime.LastTwentyFourHours != 0.5 { - t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5") - } - if serviceStatus.Uptime.LastSevenDays != 0.5 { - t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5") - } -} - -func TestStore_GetAllServiceStatuses(t *testing.T) { - store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetAllServiceStatuses.db") - defer store.db.Close() - firstResult := &testSuccessfulResult - secondResult := &testUnsuccessfulResult - store.Insert(&testService, firstResult) - store.Insert(&testService, secondResult) - // Can't be bothered dealing with timezone issues on the worker that runs the automated tests - firstResult.Timestamp = time.Time{} - secondResult.Timestamp = time.Time{} - serviceStatuses := store.GetAllServiceStatuses(paging.NewServiceStatusParams().WithResults(1, 20)) - if len(serviceStatuses) != 1 { - t.Fatal("expected 1 service status") - } - actual, exists := serviceStatuses[testService.Key()] - if !exists { - t.Fatal("expected service status to exist") - } - if len(actual.Results) != 2 { - t.Error("expected 2 results, got", len(actual.Results)) - } - if len(actual.Events) != 0 { - t.Error("expected 0 events, got", len(actual.Events)) - } -} - func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpOldUptimeEntriesProperly.db") - defer store.db.Close() + defer store.Close() now := time.Now().Round(time.Minute) now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) @@ -297,9 +204,9 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { } } -func TestStore_InsertCleansUpProperly(t *testing.T) { - store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_deleteOldServiceResults.db") - defer store.db.Close() +func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) { + store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpEventsAndResultsProperly.db") + defer store.Close() for i := 0; i < resultsCleanUpThreshold+eventsCleanUpThreshold; i++ { store.Insert(&testService, &testSuccessfulResult) store.Insert(&testService, &testUnsuccessfulResult) @@ -311,4 +218,137 @@ func TestStore_InsertCleansUpProperly(t *testing.T) { t.Errorf("number of events shouldn't have exceeded %d, reached %d", eventsCleanUpThreshold, len(ss.Events)) } } + store.Clear() +} + +func TestStore_GetServiceStatus(t *testing.T) { + store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatus.db") + defer store.Close() + firstResult := testSuccessfulResult + firstResult.Timestamp = timestamp.Add(-time.Minute) + secondResult := testUnsuccessfulResult + secondResult.Timestamp = timestamp + store.Insert(&testService, &firstResult) + store.Insert(&testService, &secondResult) + + serviceStatus := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) + if serviceStatus == nil { + t.Fatalf("serviceStatus shouldn't have been nil") + } + if serviceStatus.Uptime == nil { + t.Fatalf("serviceStatus.Uptime shouldn't have been nil") + } + if len(serviceStatus.Results) != 2 { + t.Fatalf("serviceStatus.Results should've had 2 entries") + } + if serviceStatus.Results[0].Timestamp.After(serviceStatus.Results[1].Timestamp) { + t.Fatalf("The result at index 0 should've been older than the result at index 1") + } + if serviceStatus.Uptime.LastHour != 0.5 { + t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5") + } + if serviceStatus.Uptime.LastTwentyFourHours != 0.5 { + t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5") + } + if serviceStatus.Uptime.LastSevenDays != 0.5 { + t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5") + } +} + +func TestStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { + store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatusForMissingStatusReturnsNil.db") + defer store.Close() + store.Insert(&testService, &testSuccessfulResult) + + serviceStatus := store.GetServiceStatus("nonexistantgroup", "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) + if serviceStatus != nil { + t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, testService.Name) + } + serviceStatus = store.GetServiceStatus(testService.Group, "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) + if serviceStatus != nil { + t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, "nonexistantname") + } + serviceStatus = store.GetServiceStatus("nonexistantgroup", testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) + if serviceStatus != nil { + t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", "nonexistantgroup", testService.Name) + } +} + +func TestStore_GetServiceStatusByKey(t *testing.T) { + store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatusByKey.db") + defer store.Close() + store.Insert(&testService, &testSuccessfulResult) + store.Insert(&testService, &testUnsuccessfulResult) + + serviceStatus := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) + if serviceStatus == nil { + t.Fatalf("serviceStatus shouldn't have been nil") + } + if serviceStatus.Name != testService.Name { + t.Fatalf("serviceStatus.Name should've been %s, got %s", testService.Name, serviceStatus.Name) + } + if serviceStatus.Group != testService.Group { + t.Fatalf("serviceStatus.Group should've been %s, got %s", testService.Group, serviceStatus.Group) + } + if serviceStatus.Uptime == nil { + t.Fatalf("serviceStatus.Uptime shouldn't have been nil") + } + if serviceStatus.Uptime.LastHour != 0.5 { + t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5") + } + if serviceStatus.Uptime.LastTwentyFourHours != 0.5 { + t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5") + } + if serviceStatus.Uptime.LastSevenDays != 0.5 { + t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5") + } +} + +func TestStore_GetAllServiceStatuses(t *testing.T) { + store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetAllServiceStatuses.db") + defer store.Close() + firstResult := &testSuccessfulResult + secondResult := &testUnsuccessfulResult + store.Insert(&testService, firstResult) + store.Insert(&testService, secondResult) + // Can't be bothered dealing with timezone issues on the worker that runs the automated tests + firstResult.Timestamp = time.Time{} + secondResult.Timestamp = time.Time{} + serviceStatuses := store.GetAllServiceStatuses(paging.NewServiceStatusParams().WithResults(1, 20)) + if len(serviceStatuses) != 1 { + t.Fatal("expected 1 service status") + } + actual, exists := serviceStatuses[testService.Key()] + if !exists { + t.Fatal("expected service status to exist") + } + if len(actual.Results) != 2 { + t.Error("expected 2 results, got", len(actual.Results)) + } + if len(actual.Events) != 0 { + t.Error("expected 0 events, got", len(actual.Events)) + } +} + +func TestStore_DeleteAllServiceStatusesNotInKeys(t *testing.T) { + store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_DeleteAllServiceStatusesNotInKeys.db") + defer store.Close() + firstService := core.Service{Name: "service-1", Group: "group"} + secondService := core.Service{Name: "service-2", Group: "group"} + result := &testSuccessfulResult + store.Insert(&firstService, result) + store.Insert(&secondService, result) + if store.GetServiceStatusByKey(firstService.Key(), paging.NewServiceStatusParams()) == nil { + t.Fatal("firstService should exist") + } + if store.GetServiceStatusByKey(secondService.Key(), paging.NewServiceStatusParams()) == nil { + t.Fatal("secondService should exist") + } + store.DeleteAllServiceStatusesNotInKeys([]string{firstService.Key()}) + if store.GetServiceStatusByKey(firstService.Key(), paging.NewServiceStatusParams()) == nil { + t.Error("secondService should've been deleted") + } + if store.GetServiceStatusByKey(secondService.Key(), paging.NewServiceStatusParams()) != nil { + t.Error("firstService should still exist") + } } diff --git a/storage/store/memory/memory.go b/storage/store/memory/memory.go index 4de34981..74ee9787 100644 --- a/storage/store/memory/memory.go +++ b/storage/store/memory/memory.go @@ -112,3 +112,8 @@ func (s *Store) Save() error { } return nil } + +// Close does nothing, because there's nothing to close +func (s *Store) Close() { + return +} diff --git a/storage/store/memory/util.go b/storage/store/memory/util.go index 88c7121c..421ae024 100644 --- a/storage/store/memory/util.go +++ b/storage/store/memory/util.go @@ -37,6 +37,9 @@ func ShallowCopyServiceStatus(ss *core.ServiceStatus, params *paging.ServiceStat } func getStartAndEndIndex(numberOfResults int, page, pageSize int) (int, int) { + if page < 1 || pageSize < 0 { + return -1, -1 + } start := numberOfResults - (page * pageSize) end := numberOfResults - ((page - 1) * pageSize) if start > numberOfResults { diff --git a/storage/store/memory/util_test.go b/storage/store/memory/util_test.go index e6bce8b6..480e22d3 100644 --- a/storage/store/memory/util_test.go +++ b/storage/store/memory/util_test.go @@ -11,45 +11,69 @@ import ( func TestAddResult(t *testing.T) { service := &core.Service{Name: "name", Group: "group"} serviceStatus := core.NewServiceStatus(service.Key(), service.Group, service.Name) - for i := 0; i < core.MaximumNumberOfResults+10; i++ { - AddResult(serviceStatus, &core.Result{Timestamp: time.Now()}) + for i := 0; i < (core.MaximumNumberOfResults+core.MaximumNumberOfEvents)*2; i++ { + AddResult(serviceStatus, &core.Result{Success: i%2 == 0, Timestamp: time.Now()}) } if len(serviceStatus.Results) != core.MaximumNumberOfResults { t.Errorf("expected serviceStatus.Results to not exceed a length of %d", core.MaximumNumberOfResults) } + if len(serviceStatus.Events) != core.MaximumNumberOfEvents { + t.Errorf("expected serviceStatus.Events to not exceed a length of %d", core.MaximumNumberOfEvents) + } + // Try to add nil serviceStatus + AddResult(nil, &core.Result{Timestamp: time.Now()}) } func TestShallowCopyServiceStatus(t *testing.T) { service := &core.Service{Name: "name", Group: "group"} serviceStatus := core.NewServiceStatus(service.Key(), service.Group, service.Name) + ts := time.Now().Add(-25 * time.Hour) for i := 0; i < 25; i++ { - AddResult(serviceStatus, &core.Result{Timestamp: time.Now()}) + AddResult(serviceStatus, &core.Result{Success: i%2 == 0, Timestamp: ts}) + ts = ts.Add(time.Hour) + } + if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(-1, -1)).Results) != 0 { + t.Error("expected to have 0 result") } if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(1, 1)).Results) != 1 { - t.Errorf("expected to have 1 result") + t.Error("expected to have 1 result") } if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(5, 0)).Results) != 0 { - t.Errorf("expected to have 0 results") + t.Error("expected to have 0 results") } if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(-1, 20)).Results) != 0 { - t.Errorf("expected to have 0 result, because the page was invalid") + t.Error("expected to have 0 result, because the page was invalid") } if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(1, -1)).Results) != 0 { - t.Errorf("expected to have 0 result, because the page size was invalid") + t.Error("expected to have 0 result, because the page size was invalid") } if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(1, 10)).Results) != 10 { - t.Errorf("expected to have 10 results, because given a page size of 10, page 1 should have 10 elements") + t.Error("expected to have 10 results, because given a page size of 10, page 1 should have 10 elements") } if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(2, 10)).Results) != 10 { - t.Errorf("expected to have 10 results, because given a page size of 10, page 2 should have 10 elements") + t.Error("expected to have 10 results, because given a page size of 10, page 2 should have 10 elements") } if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(3, 10)).Results) != 5 { - t.Errorf("expected to have 5 results, because given a page size of 10, page 3 should have 5 elements") + t.Error("expected to have 5 results, because given a page size of 10, page 3 should have 5 elements") } if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(4, 10)).Results) != 0 { - t.Errorf("expected to have 0 results, because given a page size of 10, page 4 should have 0 elements") + t.Error("expected to have 0 results, because given a page size of 10, page 4 should have 0 elements") } if len(ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithResults(1, 50)).Results) != 25 { - t.Errorf("expected to have 25 results, because there's only 25 results") + t.Error("expected to have 25 results, because there's only 25 results") + } + uptime := ShallowCopyServiceStatus(serviceStatus, paging.NewServiceStatusParams().WithUptime()).Uptime + if uptime == nil { + t.Error("expected uptime to not be nil") + } else { + if uptime.LastHour != 1 { + t.Error("expected uptime.LastHour to not be 1, got", uptime.LastHour) + } + if uptime.LastTwentyFourHours != 0.5 { + t.Error("expected uptime.LastTwentyFourHours to not be 0.5, got", uptime.LastTwentyFourHours) + } + if uptime.LastSevenDays != 0.52 { + t.Error("expected uptime.LastSevenDays to not be 0.52, got", uptime.LastSevenDays) + } } } diff --git a/storage/store/store.go b/storage/store/store.go index deafd6e5..c897f8f3 100644 --- a/storage/store/store.go +++ b/storage/store/store.go @@ -32,6 +32,10 @@ type Store interface { // Save persists the data if and where it needs to be persisted Save() error + + // Close terminates every connections and closes the store, if applicable. + // Should only be used before stopping the application. + Close() } // TODO: add method to check state of store (by keeping track of silent errors) diff --git a/storage/type.go b/storage/type.go new file mode 100644 index 00000000..342bad9f --- /dev/null +++ b/storage/type.go @@ -0,0 +1,9 @@ +package storage + +// Type of the store. +type Type string + +const ( + TypeInMemory Type = "inmemory" // In-memory store + TypeSQLite Type = "sqlite" // SQLite store +)