From 8ecaf4cfd52086a08ec5af3d4c07938436df1eda Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Tue, 29 Dec 2020 20:47:57 +0000 Subject: [PATCH 01/13] create an in-memory store implementation --- storage/memory.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 storage/memory.go diff --git a/storage/memory.go b/storage/memory.go new file mode 100644 index 00000000..9b4ec448 --- /dev/null +++ b/storage/memory.go @@ -0,0 +1,99 @@ +package storage + +import ( + "fmt" + "sync" + + "github.com/TwinProduction/gatus/core" +) + +var ( + serviceStatuses = make(map[string]*core.ServiceStatus) + + // serviceResultsMutex is used to prevent concurrent map access + serviceResultsMutex sync.RWMutex +) + +// InMemoryStore implements an in-memory store +type InMemoryStore struct{} + +// NewInMemoryStore returns an in-memory store. Note that the store acts as a singleton, so although new-ing +// up in-memory stores will give you a unique reference to a struct each time, all structs returned +// by this function will act on the same in-memory store. +func NewInMemoryStore() InMemoryStore { + return InMemoryStore{} +} + +// GetAll returns all the observed results for all services from the in memory store +func (ims *InMemoryStore) GetAll() map[string]*core.ServiceStatus { + results := make(map[string]*core.ServiceStatus) + serviceResultsMutex.RLock() + for key, svcStatus := range serviceStatuses { + copiedResults := copyResults(svcStatus.Results) + results[key] = &core.ServiceStatus{ + Name: svcStatus.Name, + Group: svcStatus.Group, + Results: copiedResults, + } + } + serviceResultsMutex.RUnlock() + + return results +} + +// Insert inserts the observed result for the specified service into the in memory store +func (ims *InMemoryStore) Insert(service *core.Service, result *core.Result) { + key := fmt.Sprintf("%s_%s", service.Group, service.Name) + serviceResultsMutex.Lock() + serviceStatus, exists := serviceStatuses[key] + if !exists { + serviceStatus = core.NewServiceStatus(service) + serviceStatuses[key] = serviceStatus + } + serviceStatus.AddResult(result) + serviceResultsMutex.Unlock() +} + +func copyResults(results []*core.Result) []*core.Result { + copiedResults := []*core.Result{} + for _, result := range results { + copiedErrors := copyErrors(result.Errors) + copiedConditionResults := copyConditionResults(result.ConditionResults) + + copiedResults = append(copiedResults, &core.Result{ + HTTPStatus: result.HTTPStatus, + DNSRCode: result.DNSRCode, + Body: result.Body, + Hostname: result.Hostname, + IP: result.IP, + Connected: result.Connected, + Duration: result.Duration, + Errors: copiedErrors, + ConditionResults: copiedConditionResults, + Success: result.Connected, + Timestamp: result.Timestamp, + CertificateExpiration: result.CertificateExpiration, + }) + } + return copiedResults +} + +func copyConditionResults(crs []*core.ConditionResult) []*core.ConditionResult { + copiedConditionResults := []*core.ConditionResult{} + for _, conditionResult := range crs { + copiedConditionResults = append(copiedConditionResults, &core.ConditionResult{ + Condition: conditionResult.Condition, + Success: conditionResult.Success, + }) + } + + return copiedConditionResults +} + +func copyErrors(errors []string) []string { + copiedErrors := []string{} + for _, error := range errors { + copiedErrors = append(copiedErrors, error) + } + return copiedErrors +} From c3bc375ff185f61c8fbf7a4a383d8eab5fd967df Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Wed, 30 Dec 2020 10:57:17 +0000 Subject: [PATCH 02/13] add memory tests --- storage/memory.go | 7 + storage/memory_test.go | 404 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 storage/memory_test.go diff --git a/storage/memory.go b/storage/memory.go index 9b4ec448..f9688dc0 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -97,3 +97,10 @@ func copyErrors(errors []string) []string { } return copiedErrors } + +// Clear will empty all the results from the in memory store +func (ims *InMemoryStore) Clear() { + serviceResultsMutex.Lock() + serviceStatuses = make(map[string]*core.ServiceStatus) + serviceResultsMutex.Unlock() +} diff --git a/storage/memory_test.go b/storage/memory_test.go new file mode 100644 index 00000000..6fe83e5d --- /dev/null +++ b/storage/memory_test.go @@ -0,0 +1,404 @@ +package storage + +import ( + "fmt" + "testing" + "time" + + "github.com/TwinProduction/gatus/core" +) + +var testService = core.Service{ + Name: "Name", + Group: "Group", + URL: "URL", + DNS: &core.DNS{QueryType: "QueryType", QueryName: "QueryName"}, + Method: "Method", + Body: "Body", + GraphQL: false, + Headers: nil, + Interval: time.Second * 2, + Conditions: nil, + Alerts: nil, + Insecure: false, + NumberOfFailuresInARow: 0, + NumberOfSuccessesInARow: 0, +} + +var memoryStore = NewInMemoryStore() + +func TestStorage_GetAllFromEmptyMemoryStoreReturnsNothing(t *testing.T) { + memoryStore.Clear() + results := memoryStore.GetAll() + if len(results) != 0 { + t.Errorf("MemoryStore should've returned 0 results, but actually returned %d", len(results)) + } +} + +func TestStorage_InsertIntoEmptyMemoryStoreThenGetAllReturnsOneResult(t *testing.T) { + memoryStore.Clear() + result := core.Result{ + HTTPStatus: 200, + DNSRCode: "DNSRCode", + Body: nil, + Hostname: "Hostname", + IP: "IP", + Connected: false, + Duration: time.Second * 2, + Errors: nil, + ConditionResults: nil, + Success: false, + Timestamp: time.Now(), + CertificateExpiration: time.Second * 2, + } + + memoryStore.Insert(&testService, &result) + + results := memoryStore.GetAll() + if len(results) != 1 { + t.Errorf("MemoryStore should've returned 0 results, but actually returned %d", len(results)) + } + + key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) + storedResult, exists := results[key] + if !exists { + t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) + } + + if storedResult.Name != testService.Name { + t.Errorf("Stored Results Name should've been %s, but was %s", testService.Name, storedResult.Name) + } + if storedResult.Group != testService.Group { + t.Errorf("Stored Results Group should've been %s, but was %s", testService.Group, storedResult.Group) + } + if len(storedResult.Results) != 1 { + t.Errorf("Stored Results for service %s should've had 1 result, but actually had %d", storedResult.Name, len(storedResult.Results)) + } + if storedResult.Results[0] == &result { + t.Errorf("Returned result is the same reference as result passed to insert. Returned result should be copies only") + } +} + +func TestStorage_InsertTwoResultsForSingleServiceIntoEmptyMemoryStore_ThenGetAllReturnsTwoResults(t *testing.T) { + memoryStore.Clear() + result1 := core.Result{ + HTTPStatus: 404, + DNSRCode: "DNSRCode", + Body: nil, + Hostname: "Hostname", + IP: "IP", + Connected: false, + Duration: time.Second * 2, + Errors: nil, + ConditionResults: nil, + Success: false, + Timestamp: time.Now(), + CertificateExpiration: time.Second * 2, + } + result2 := core.Result{ + HTTPStatus: 200, + DNSRCode: "DNSRCode", + Body: nil, + Hostname: "Hostname", + IP: "IP", + Connected: true, + Duration: time.Second * 2, + Errors: nil, + ConditionResults: nil, + Success: true, + Timestamp: time.Now(), + CertificateExpiration: time.Second * 2, + } + + resultsToInsert := []core.Result{result1, result2} + + memoryStore.Insert(&testService, &result1) + memoryStore.Insert(&testService, &result2) + + results := memoryStore.GetAll() + if len(results) != 1 { + t.Fatalf("MemoryStore should've returned 1 results, but actually returned %d", len(results)) + } + + key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) + serviceResults, exists := results[key] + if !exists { + t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) + } + + if len(serviceResults.Results) != 2 { + t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceResults.Name, len(serviceResults.Results)) + } + + for i, r := range serviceResults.Results { + expectedResult := resultsToInsert[i] + + if r.HTTPStatus != expectedResult.HTTPStatus { + t.Errorf("Result at index %d should've had a HTTPStatus of %d, but was actually %d", i, expectedResult.HTTPStatus, r.HTTPStatus) + } + if r.DNSRCode != expectedResult.DNSRCode { + t.Errorf("Result at index %d should've had a DNSRCode of %s, but was actually %s", i, expectedResult.DNSRCode, r.DNSRCode) + } + if len(r.Body) != len(expectedResult.Body) { + t.Errorf("Result at index %d should've had a body of length %d, but was actually %d", i, len(expectedResult.Body), len(r.Body)) + } + if r.Hostname != expectedResult.Hostname { + t.Errorf("Result at index %d should've had a Hostname of %s, but was actually %s", i, expectedResult.Hostname, r.Hostname) + } + if r.IP != expectedResult.IP { + t.Errorf("Result at index %d should've had a IP of %s, but was actually %s", i, expectedResult.IP, r.IP) + } + if r.Connected != expectedResult.Connected { + t.Errorf("Result at index %d should've had a Connected value of %t, but was actually %t", i, expectedResult.Connected, r.Connected) + } + if r.Duration != expectedResult.Duration { + t.Errorf("Result at index %d should've had a Duration of %s, but was actually %s", i, expectedResult.Duration.String(), r.Duration.String()) + } + if len(r.Errors) != len(expectedResult.Errors) { + t.Errorf("Result at index %d should've had %d errors, but actually had %d errors", i, len(expectedResult.Errors), len(r.Errors)) + } + if len(r.ConditionResults) != len(expectedResult.ConditionResults) { + t.Errorf("Result at index %d should've had %d ConditionResults, but actually had %d ConditionResults", i, len(expectedResult.ConditionResults), len(r.ConditionResults)) + } + if r.Success != expectedResult.Success { + t.Errorf("Result at index %d should've had a Success of %t, but was actually %t", i, expectedResult.Success, r.Success) + } + if r.Timestamp != expectedResult.Timestamp { + t.Errorf("Result at index %d should've had a Timestamp of %s, but was actually %s", i, expectedResult.Timestamp.String(), r.Timestamp.String()) + } + if r.CertificateExpiration != expectedResult.CertificateExpiration { + t.Errorf("Result at index %d should've had a CertificateExpiration of %s, but was actually %s", i, expectedResult.CertificateExpiration.String(), r.CertificateExpiration.String()) + } + } +} + +func TestStorage_InsertTwoResultsTwoServicesIntoEmptyMemoryStore_ThenGetAllReturnsTwoServicesWithOneResultEach(t *testing.T) { + memoryStore.Clear() + result1 := core.Result{ + HTTPStatus: 404, + DNSRCode: "DNSRCode", + Body: nil, + Hostname: "Hostname", + IP: "IP", + Connected: false, + Duration: time.Second * 2, + Errors: nil, + ConditionResults: nil, + Success: false, + Timestamp: time.Now(), + CertificateExpiration: time.Second * 2, + } + result2 := core.Result{ + HTTPStatus: 200, + DNSRCode: "DNSRCode", + Body: nil, + Hostname: "Hostname", + IP: "IP", + Connected: true, + Duration: time.Second * 2, + Errors: nil, + ConditionResults: nil, + Success: true, + Timestamp: time.Now(), + CertificateExpiration: time.Second * 2, + } + + testService2 := core.Service{ + Name: "Name2", + Group: "Group", + URL: "URL", + DNS: &core.DNS{QueryType: "QueryType", QueryName: "QueryName"}, + Method: "Method", + Body: "Body", + GraphQL: false, + Headers: nil, + Interval: time.Second * 2, + Conditions: nil, + Alerts: nil, + Insecure: false, + NumberOfFailuresInARow: 0, + NumberOfSuccessesInARow: 0, + } + + memoryStore.Insert(&testService, &result1) + memoryStore.Insert(&testService2, &result2) + + results := memoryStore.GetAll() + if len(results) != 2 { + t.Fatalf("MemoryStore should've returned 2 results, but actually returned %d", len(results)) + } + + key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) + serviceResults1, exists := results[key] + if !exists { + t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) + } + + if len(serviceResults1.Results) != 1 { + t.Fatalf("Service '%s' should've had 1 results, but actually returned %d", serviceResults1.Name, len(serviceResults1.Results)) + } + + key = fmt.Sprintf("%s_%s", testService2.Group, testService2.Name) + serviceResults2, exists := results[key] + if !exists { + t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) + } + + if len(serviceResults2.Results) != 1 { + t.Fatalf("Service '%s' should've had 1 results, but actually returned %d", serviceResults1.Name, len(serviceResults1.Results)) + } +} + +func TestStorage_InsertResultForServiceWithErrorsIntoEmptyMemoryStore_ThenGetAllReturnsOneResultWithErrors(t *testing.T) { + memoryStore.Clear() + errors := []string{ + "error1", + "error2", + } + result1 := core.Result{ + HTTPStatus: 404, + DNSRCode: "DNSRCode", + Body: nil, + Hostname: "Hostname", + IP: "IP", + Connected: false, + Duration: time.Second * 2, + Errors: errors, + ConditionResults: nil, + Success: false, + Timestamp: time.Now(), + CertificateExpiration: time.Second * 2, + } + + memoryStore.Insert(&testService, &result1) + + results := memoryStore.GetAll() + if len(results) != 1 { + t.Fatalf("MemoryStore should've returned 1 results, but actually returned %d", len(results)) + } + + key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) + serviceResults, exists := results[key] + if !exists { + t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) + } + + if len(serviceResults.Results) != 1 { + t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceResults.Name, len(serviceResults.Results)) + } + + actualResult := serviceResults.Results[0] + + if len(actualResult.Errors) != len(errors) { + t.Errorf("Service result should've had 2 errors, but actually had %d errors", len(actualResult.Errors)) + } + + for i, err := range actualResult.Errors { + if err != errors[i] { + t.Errorf("Error at index %d should've been %s, but was actually %s", i, errors[i], err) + } + } +} + +func TestStorage_InsertResultForServiceWithConditionResultsIntoEmptyMemoryStore_ThenGetAllReturnsOneResultWithConditionResults(t *testing.T) { + memoryStore.Clear() + crs := []*core.ConditionResult{ + { + Condition: "condition1", + Success: true, + }, + { + Condition: "condition2", + Success: false, + }, + } + result := core.Result{ + HTTPStatus: 404, + DNSRCode: "DNSRCode", + Body: nil, + Hostname: "Hostname", + IP: "IP", + Connected: false, + Duration: time.Second * 2, + Errors: nil, + ConditionResults: crs, + Success: false, + Timestamp: time.Now(), + CertificateExpiration: time.Second * 2, + } + + memoryStore.Insert(&testService, &result) + + results := memoryStore.GetAll() + if len(results) != 1 { + t.Fatalf("MemoryStore should've returned 1 results, but actually returned %d", len(results)) + } + + key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) + serviceResults, exists := results[key] + if !exists { + t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) + } + + if len(serviceResults.Results) != 1 { + t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceResults.Name, len(serviceResults.Results)) + } + + actualResult := serviceResults.Results[0] + + if len(actualResult.ConditionResults) != len(crs) { + t.Errorf("Service result should've had 2 ConditionResults, but actually had %d ConditionResults", len(actualResult.Errors)) + } + + for i, cr := range actualResult.ConditionResults { + if cr.Condition != crs[i].Condition { + t.Errorf("ConditionResult at index %d should've had condition %s, but was actually %s", i, crs[i].Condition, cr.Condition) + } + if cr.Success != crs[i].Success { + t.Errorf("ConditionResult at index %d should've had success value of %t, but was actually %t", i, crs[i].Success, cr.Success) + } + } +} + +func TestStorage_MultipleMemoryStoreInstancesReferToSameMemoryMap(t *testing.T) { + memoryStore.Clear() + currentMap := memoryStore.GetAll() + + otherMemoryStore := NewInMemoryStore() + otherMemoryStoresMap := otherMemoryStore.GetAll() + + if len(currentMap) != len(otherMemoryStoresMap) { + t.Errorf("Multiple memory stores should refer to the same internal map, but 'memoryStore' returned %d results, and 'otherMemoryStore' returned %d results", len(currentMap), len(otherMemoryStoresMap)) + } + + memoryStore.Insert(&testService, &core.Result{}) + currentMap = memoryStore.GetAll() + otherMemoryStoresMap = otherMemoryStore.GetAll() + + if len(currentMap) != len(otherMemoryStoresMap) { + t.Errorf("Multiple memory stores should refer to the same internal map, but 'memoryStore' returned %d results after inserting, and 'otherMemoryStore' returned %d results after inserting", len(currentMap), len(otherMemoryStoresMap)) + } + + otherMemoryStore.Clear() + currentMap = memoryStore.GetAll() + otherMemoryStoresMap = otherMemoryStore.GetAll() + + if len(currentMap) != len(otherMemoryStoresMap) { + t.Errorf("Multiple memory stores should refer to the same internal map, but 'memoryStore' returned %d results after clearing, and 'otherMemoryStore' returned %d results after clearing", len(currentMap), len(otherMemoryStoresMap)) + } +} + +func TestStorage_ModificationsToReturnedMapDoNotAffectInternalMap(t *testing.T) { + memoryStore.Clear() + + memoryStore.Insert(&testService, &core.Result{}) + modifiedResults := memoryStore.GetAll() + for k := range modifiedResults { + delete(modifiedResults, k) + } + results := memoryStore.GetAll() + + if len(modifiedResults) == len(results) { + t.Errorf("Returned map from GetAll should be free to modify by the caller without affecting internal in-memory map, but length of results from in-memory map (%d) was equal to the length of results in modified map (%d)", len(results), len(modifiedResults)) + } +} From f8e1fc25a4dca178de7f7896f2a33047e7e9bb3b Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Thu, 31 Dec 2020 11:49:41 +0000 Subject: [PATCH 03/13] use the new store in the watchdog --- watchdog/watchdog.go | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/watchdog/watchdog.go b/watchdog/watchdog.go index 89a28e33..d517e9e3 100644 --- a/watchdog/watchdog.go +++ b/watchdog/watchdog.go @@ -10,13 +10,11 @@ import ( "github.com/TwinProduction/gatus/config" "github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/metric" + "github.com/TwinProduction/gatus/storage" ) var ( - serviceStatuses = make(map[string]*core.ServiceStatus) - - // serviceStatusesMutex is used to prevent concurrent map access - serviceStatusesMutex sync.RWMutex + store = storage.NewInMemoryStore() // monitoringMutex is used to prevent multiple services from being evaluated at the same time. // Without this, conditions using response time may become inaccurate. @@ -26,18 +24,16 @@ var ( // GetJSONEncodedServiceStatuses returns a list of core.ServiceStatus for each services encoded using json.Marshal. // The reason why the encoding is done here is because we use a mutex to prevent concurrent map access. func GetJSONEncodedServiceStatuses() ([]byte, error) { - serviceStatusesMutex.RLock() + serviceStatuses := store.GetAll() data, err := json.Marshal(serviceStatuses) - serviceStatusesMutex.RUnlock() return data, err } // GetUptimeByServiceGroupAndName returns the uptime of a service based on its group and name func GetUptimeByServiceGroupAndName(group, name string) *core.Uptime { key := fmt.Sprintf("%s_%s", group, name) - serviceStatusesMutex.RLock() + serviceStatuses := store.GetAll() serviceStatus, exists := serviceStatuses[key] - serviceStatusesMutex.RUnlock() if !exists { return nil } @@ -93,13 +89,5 @@ func monitor(service *core.Service) { // UpdateServiceStatuses updates the slice of service statuses func UpdateServiceStatuses(service *core.Service, result *core.Result) { - key := fmt.Sprintf("%s_%s", service.Group, service.Name) - serviceStatusesMutex.Lock() - serviceStatus, exists := serviceStatuses[key] - if !exists { - serviceStatus = core.NewServiceStatus(service) - serviceStatuses[key] = serviceStatus - } - serviceStatus.AddResult(result) - serviceStatusesMutex.Unlock() + store.Insert(service, result) } From 678b78ac014a772820993190befc7b3c82b34a29 Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Thu, 31 Dec 2020 12:01:48 +0000 Subject: [PATCH 04/13] off-topic: add artifact to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 48ae5ece..21fb169f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin .idea .vscode +gatus \ No newline at end of file From dbd95b1bbdab776796df02d887b32cd016867904 Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Thu, 31 Dec 2020 12:05:57 +0000 Subject: [PATCH 05/13] remove no longer valid comment The results returned from the in-memory map are copies, so there's no concern over concurrent map access anymore, as the internal memory-map is hidden and inaccessible to callers --- watchdog/watchdog.go | 1 - 1 file changed, 1 deletion(-) diff --git a/watchdog/watchdog.go b/watchdog/watchdog.go index d517e9e3..091a71e8 100644 --- a/watchdog/watchdog.go +++ b/watchdog/watchdog.go @@ -22,7 +22,6 @@ var ( ) // GetJSONEncodedServiceStatuses returns a list of core.ServiceStatus for each services encoded using json.Marshal. -// The reason why the encoding is done here is because we use a mutex to prevent concurrent map access. func GetJSONEncodedServiceStatuses() ([]byte, error) { serviceStatuses := store.GetAll() data, err := json.Marshal(serviceStatuses) From 5eb289c4d30b2b75b2936d711227c61d39167777 Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Thu, 31 Dec 2020 20:28:57 +0000 Subject: [PATCH 06/13] rename GetJSONEncodedServiceStatuses -> GetServiceStatusesAsJSON --- controller/controller.go | 2 +- watchdog/watchdog.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/controller/controller.go b/controller/controller.go index f12786d8..4ec7c4fc 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -79,7 +79,7 @@ func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) { var err error buffer := &bytes.Buffer{} gzipWriter := gzip.NewWriter(buffer) - data, err = watchdog.GetJSONEncodedServiceStatuses() + data, err = watchdog.GetServiceStatusesAsJSON() if err != nil { log.Printf("[main][serviceStatusesHandler] Unable to marshal object to JSON: %s", err.Error()) writer.WriteHeader(http.StatusInternalServerError) diff --git a/watchdog/watchdog.go b/watchdog/watchdog.go index 091a71e8..408406b8 100644 --- a/watchdog/watchdog.go +++ b/watchdog/watchdog.go @@ -21,8 +21,8 @@ var ( monitoringMutex sync.Mutex ) -// GetJSONEncodedServiceStatuses returns a list of core.ServiceStatus for each services encoded using json.Marshal. -func GetJSONEncodedServiceStatuses() ([]byte, error) { +// GetServiceStatusesAsJSON returns a list of core.ServiceStatus for each services encoded using json.Marshal. +func GetServiceStatusesAsJSON() ([]byte, error) { serviceStatuses := store.GetAll() data, err := json.Marshal(serviceStatuses) return data, err From 4d24a4d6478e22692de1100180b683f9cf2bda8b Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Thu, 31 Dec 2020 20:33:35 +0000 Subject: [PATCH 07/13] add func to store for getting single service status and use that in the watchdog --- storage/memory.go | 12 ++++++++++++ watchdog/watchdog.go | 6 ++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/storage/memory.go b/storage/memory.go index f9688dc0..8a50ff8c 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -41,6 +41,18 @@ func (ims *InMemoryStore) GetAll() map[string]*core.ServiceStatus { return results } +// GetServiceStatus returns the service status for a given service name in the given group +func (ims *InMemoryStore) GetServiceStatus(group, name string) *core.ServiceStatus { + key := fmt.Sprintf("%s_%s", group, name) + serviceResultsMutex.RLock() + serviceStatus, exists := serviceStatuses[key] + serviceResultsMutex.RUnlock() + if !exists { + return nil + } + return serviceStatus +} + // Insert inserts the observed result for the specified service into the in memory store func (ims *InMemoryStore) Insert(service *core.Service, result *core.Result) { key := fmt.Sprintf("%s_%s", service.Group, service.Name) diff --git a/watchdog/watchdog.go b/watchdog/watchdog.go index 408406b8..7e7664f0 100644 --- a/watchdog/watchdog.go +++ b/watchdog/watchdog.go @@ -30,10 +30,8 @@ func GetServiceStatusesAsJSON() ([]byte, error) { // GetUptimeByServiceGroupAndName returns the uptime of a service based on its group and name func GetUptimeByServiceGroupAndName(group, name string) *core.Uptime { - key := fmt.Sprintf("%s_%s", group, name) - serviceStatuses := store.GetAll() - serviceStatus, exists := serviceStatuses[key] - if !exists { + serviceStatus := store.GetServiceStatus(group, name) + if serviceStatus == nil { return nil } return serviceStatus.Uptime From 029c87df890eade2351c5391f719934cdba1750d Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Thu, 31 Dec 2020 20:43:11 +0000 Subject: [PATCH 08/13] add tests for new func --- storage/memory_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/storage/memory_test.go b/storage/memory_test.go index 6fe83e5d..066b3edd 100644 --- a/storage/memory_test.go +++ b/storage/memory_test.go @@ -402,3 +402,35 @@ func TestStorage_ModificationsToReturnedMapDoNotAffectInternalMap(t *testing.T) t.Errorf("Returned map from GetAll should be free to modify by the caller without affecting internal in-memory map, but length of results from in-memory map (%d) was equal to the length of results in modified map (%d)", len(results), len(modifiedResults)) } } + +func TestStorage_GetServiceStatusForExistingStatusReturnsThatServiceStatus(t *testing.T) { + memoryStore.Clear() + + memoryStore.Insert(&testService, &core.Result{}) + serviceStatus := memoryStore.GetServiceStatus(testService.Group, testService.Name) + + if serviceStatus == nil { + t.Errorf("Returned service status for group '%s' and name '%s' was nil after inserting the service into the store", testService.Group, testService.Name) + } +} + +func TestStorage_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { + memoryStore.Clear() + + memoryStore.Insert(&testService, &core.Result{}) + + serviceStatus := memoryStore.GetServiceStatus("nonexistantgroup", "nonexistantname") + 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 = memoryStore.GetServiceStatus(testService.Group, "nonexistantname") + 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 = memoryStore.GetServiceStatus("nonexistantgroup", testService.Name) + 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) + } +} From 8ca9fd7db5d250c4ce90acb1a611a2fcf23d4dc1 Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Thu, 31 Dec 2020 20:37:11 +0000 Subject: [PATCH 09/13] make each memory store struct have its own internal map effectively removing the global state --- storage/memory.go | 42 ++++++++++++++++++++---------------------- storage/memory_test.go | 12 ++++++------ 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/storage/memory.go b/storage/memory.go index 8a50ff8c..669a4bdb 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -7,28 +7,26 @@ import ( "github.com/TwinProduction/gatus/core" ) -var ( - serviceStatuses = make(map[string]*core.ServiceStatus) - - // serviceResultsMutex is used to prevent concurrent map access - serviceResultsMutex sync.RWMutex -) - // InMemoryStore implements an in-memory store -type InMemoryStore struct{} +type InMemoryStore struct { + serviceStatuses map[string]*core.ServiceStatus + serviceResultsMutex sync.RWMutex +} // NewInMemoryStore returns an in-memory store. Note that the store acts as a singleton, so although new-ing // up in-memory stores will give you a unique reference to a struct each time, all structs returned // by this function will act on the same in-memory store. func NewInMemoryStore() InMemoryStore { - return InMemoryStore{} + return InMemoryStore{ + serviceStatuses: make(map[string]*core.ServiceStatus), + } } // GetAll returns all the observed results for all services from the in memory store func (ims *InMemoryStore) GetAll() map[string]*core.ServiceStatus { results := make(map[string]*core.ServiceStatus) - serviceResultsMutex.RLock() - for key, svcStatus := range serviceStatuses { + ims.serviceResultsMutex.RLock() + for key, svcStatus := range ims.serviceStatuses { copiedResults := copyResults(svcStatus.Results) results[key] = &core.ServiceStatus{ Name: svcStatus.Name, @@ -36,7 +34,7 @@ func (ims *InMemoryStore) GetAll() map[string]*core.ServiceStatus { Results: copiedResults, } } - serviceResultsMutex.RUnlock() + ims.serviceResultsMutex.RUnlock() return results } @@ -44,9 +42,9 @@ func (ims *InMemoryStore) GetAll() map[string]*core.ServiceStatus { // GetServiceStatus returns the service status for a given service name in the given group func (ims *InMemoryStore) GetServiceStatus(group, name string) *core.ServiceStatus { key := fmt.Sprintf("%s_%s", group, name) - serviceResultsMutex.RLock() - serviceStatus, exists := serviceStatuses[key] - serviceResultsMutex.RUnlock() + ims.serviceResultsMutex.RLock() + serviceStatus, exists := ims.serviceStatuses[key] + ims.serviceResultsMutex.RUnlock() if !exists { return nil } @@ -56,14 +54,14 @@ func (ims *InMemoryStore) GetServiceStatus(group, name string) *core.ServiceStat // Insert inserts the observed result for the specified service into the in memory store func (ims *InMemoryStore) Insert(service *core.Service, result *core.Result) { key := fmt.Sprintf("%s_%s", service.Group, service.Name) - serviceResultsMutex.Lock() - serviceStatus, exists := serviceStatuses[key] + ims.serviceResultsMutex.Lock() + serviceStatus, exists := ims.serviceStatuses[key] if !exists { serviceStatus = core.NewServiceStatus(service) - serviceStatuses[key] = serviceStatus + ims.serviceStatuses[key] = serviceStatus } serviceStatus.AddResult(result) - serviceResultsMutex.Unlock() + ims.serviceResultsMutex.Unlock() } func copyResults(results []*core.Result) []*core.Result { @@ -112,7 +110,7 @@ func copyErrors(errors []string) []string { // Clear will empty all the results from the in memory store func (ims *InMemoryStore) Clear() { - serviceResultsMutex.Lock() - serviceStatuses = make(map[string]*core.ServiceStatus) - serviceResultsMutex.Unlock() + ims.serviceResultsMutex.Lock() + ims.serviceStatuses = make(map[string]*core.ServiceStatus) + ims.serviceResultsMutex.Unlock() } diff --git a/storage/memory_test.go b/storage/memory_test.go index 066b3edd..2f7ac157 100644 --- a/storage/memory_test.go +++ b/storage/memory_test.go @@ -360,7 +360,7 @@ func TestStorage_InsertResultForServiceWithConditionResultsIntoEmptyMemoryStore_ } } -func TestStorage_MultipleMemoryStoreInstancesReferToSameMemoryMap(t *testing.T) { +func TestStorage_MultipleMemoryStoreInstancesReferToDifferentInternalMaps(t *testing.T) { memoryStore.Clear() currentMap := memoryStore.GetAll() @@ -368,23 +368,23 @@ func TestStorage_MultipleMemoryStoreInstancesReferToSameMemoryMap(t *testing.T) otherMemoryStoresMap := otherMemoryStore.GetAll() if len(currentMap) != len(otherMemoryStoresMap) { - t.Errorf("Multiple memory stores should refer to the same internal map, but 'memoryStore' returned %d results, and 'otherMemoryStore' returned %d results", len(currentMap), len(otherMemoryStoresMap)) + t.Errorf("Multiple memory stores should refer to the different internal maps, but 'memoryStore' returned %d results, and 'otherMemoryStore' returned %d results", len(currentMap), len(otherMemoryStoresMap)) } memoryStore.Insert(&testService, &core.Result{}) currentMap = memoryStore.GetAll() otherMemoryStoresMap = otherMemoryStore.GetAll() - if len(currentMap) != len(otherMemoryStoresMap) { - t.Errorf("Multiple memory stores should refer to the same internal map, but 'memoryStore' returned %d results after inserting, and 'otherMemoryStore' returned %d results after inserting", len(currentMap), len(otherMemoryStoresMap)) + if len(currentMap) == len(otherMemoryStoresMap) { + t.Errorf("Multiple memory stores should refer to different internal maps, but 'memoryStore' returned %d results after inserting, and 'otherMemoryStore' returned %d results after inserting", len(currentMap), len(otherMemoryStoresMap)) } otherMemoryStore.Clear() currentMap = memoryStore.GetAll() otherMemoryStoresMap = otherMemoryStore.GetAll() - if len(currentMap) != len(otherMemoryStoresMap) { - t.Errorf("Multiple memory stores should refer to the same internal map, but 'memoryStore' returned %d results after clearing, and 'otherMemoryStore' returned %d results after clearing", len(currentMap), len(otherMemoryStoresMap)) + if len(currentMap) == len(otherMemoryStoresMap) { + t.Errorf("Multiple memory stores should refer to different internal maps, but 'memoryStore' returned %d results after clearing, and 'otherMemoryStore' returned %d results after clearing", len(currentMap), len(otherMemoryStoresMap)) } } From 0af88377109a8895db5bb7d33e70d488f7ecaabd Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Thu, 31 Dec 2020 20:49:13 +0000 Subject: [PATCH 10/13] return pointer for consistency --- storage/memory.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/storage/memory.go b/storage/memory.go index 669a4bdb..72f24324 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -16,8 +16,8 @@ type InMemoryStore struct { // NewInMemoryStore returns an in-memory store. Note that the store acts as a singleton, so although new-ing // up in-memory stores will give you a unique reference to a struct each time, all structs returned // by this function will act on the same in-memory store. -func NewInMemoryStore() InMemoryStore { - return InMemoryStore{ +func NewInMemoryStore() *InMemoryStore { + return &InMemoryStore{ serviceStatuses: make(map[string]*core.ServiceStatus), } } From fc4858b1a833be6b60b27cc9be8d88bf2a2825fc Mon Sep 17 00:00:00 2001 From: Chris Heppell <12884767+cjheppell@users.noreply.github.com> Date: Thu, 31 Dec 2020 21:56:06 +0000 Subject: [PATCH 11/13] Update storage/memory.go Co-authored-by: Chris C. --- storage/memory.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/storage/memory.go b/storage/memory.go index 72f24324..0a082afb 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -43,11 +43,8 @@ func (ims *InMemoryStore) GetAll() map[string]*core.ServiceStatus { func (ims *InMemoryStore) GetServiceStatus(group, name string) *core.ServiceStatus { key := fmt.Sprintf("%s_%s", group, name) ims.serviceResultsMutex.RLock() - serviceStatus, exists := ims.serviceStatuses[key] + serviceStatus := ims.serviceStatuses[key] ims.serviceResultsMutex.RUnlock() - if !exists { - return nil - } return serviceStatus } From fb5477f50bc887b5103b8e648d1a02b45d1f3eea Mon Sep 17 00:00:00 2001 From: Chris Heppell Date: Thu, 31 Dec 2020 22:00:38 +0000 Subject: [PATCH 12/13] inline json.Marshal return --- watchdog/watchdog.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/watchdog/watchdog.go b/watchdog/watchdog.go index 7e7664f0..b307b140 100644 --- a/watchdog/watchdog.go +++ b/watchdog/watchdog.go @@ -24,8 +24,7 @@ var ( // GetServiceStatusesAsJSON returns a list of core.ServiceStatus for each services encoded using json.Marshal. func GetServiceStatusesAsJSON() ([]byte, error) { serviceStatuses := store.GetAll() - data, err := json.Marshal(serviceStatuses) - return data, err + return json.Marshal(serviceStatuses) } // GetUptimeByServiceGroupAndName returns the uptime of a service based on its group and name From 7f647305ce0974c7420dceb92b1281dabb735c17 Mon Sep 17 00:00:00 2001 From: Chris Heppell <12884767+cjheppell@users.noreply.github.com> Date: Thu, 31 Dec 2020 22:31:51 +0000 Subject: [PATCH 13/13] Update storage/memory.go Co-authored-by: Chris C. --- storage/memory.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage/memory.go b/storage/memory.go index 0a082afb..92ecb077 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -24,7 +24,7 @@ func NewInMemoryStore() *InMemoryStore { // GetAll returns all the observed results for all services from the in memory store func (ims *InMemoryStore) GetAll() map[string]*core.ServiceStatus { - results := make(map[string]*core.ServiceStatus) + results := make(map[string]*core.ServiceStatus, len(ims.serviceStatuses)) ims.serviceResultsMutex.RLock() for key, svcStatus := range ims.serviceStatuses { copiedResults := copyResults(svcStatus.Results)