From 10bd9ee953f90aefc14349104a47010d45502a3b Mon Sep 17 00:00:00 2001 From: Raj Nandan Sharma Date: Thu, 7 Mar 2024 19:01:55 +0530 Subject: [PATCH] rewrtten concurrency --- .gitignore | 29 +++ README.md | 252 +++++++++++++++-------- tripper.go | 310 ++++++++++++++++------------ tripper_test.go | 530 +++++++++++++++++++++++++++++------------------- 4 files changed, 702 insertions(+), 419 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fba9a9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +.DS_Store +.vscode/ +coverage.out +coverage.html +main/ +.idea/ +vendor/ +bin/ \ No newline at end of file diff --git a/README.md b/README.md index 156ba3b..0f8d8c3 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,231 @@ -# go-tripper + +# Tripper -![Tripper](https://github.com/rajnandan1/go-tripper/assets/16224367/ba0b329c-6aa7-4d5f-aa35-4f7d28e9e3bf) +Tripper is a circuit breaker package for Go that allows you to monitor and control the status of circuits. +![Tripper](https://github.com/rajnandan1/go-tripper/assets/16224367/ba0b329c-6aa7-4d5f-aa35-4f7d28e9e3bf) [![Coverage Status](https://coveralls.io/repos/github/rajnandan1/go-tripper/badge.svg?branch=main)](https://coveralls.io/github/rajnandan1/go-tripper?branch=main) -The `tripper` is a lightweight, 0 dependencies package that provides functionality for monitoring the status of a circuit. It allows you to track the success and failure counts of events and determine if a circuit should be open or closed based on configurable thresholds. This can be useful in scenarios where you want to implement circuit breaker patterns or manage the availability of services. +## Installation -### Installation +To install Tripper, use `go get`: -```bash +```shell go get github.com/rajnandan1/go-tripper ``` -## How to Use +## Usage -1. Import the `tripper` package into your Go code: +Import the Tripper package in your Go code: ```go import "github.com/rajnandan1/go-tripper" ``` -2. Create a new `Monitor` instance with the desired configuration: +### Creating a Tripper + +To create a Tripper instance, use the `Configure` function: ```go -monitor := tripper.Monitor{ - Name: "MyMonitor", - Threshold: 0.5, - ThresholdType: tripper.ThresholdPercentage, - MinimumCount: 10, - IntervalInSeconds: 60, -} +tripper := tripper.Configure(tripper.TripperOptions{}) ``` -3. Add the monitor to the circuits map using the `AddMonitor` function: +### Adding a Monitor +To add a monitor to the Tripper, use the `AddMonitor` function: +#### Monitor With Count ```go -err := tripper.AddMonitor(monitor) -if err != nil { - // Handle error +//Adding a monitor that will trip the circuit if count of failure is more than 10 in 1 minute +//for a minimum of 100 count +monitorOptions := tripper.MonitorOptions{ + Name: "example-monitor", + Threshold: 10, + ThresholdType: tripper.ThresholdCount, + MinimumCount: 100, + IntervalInSeconds: 60, + OnCircuitOpen: onCircuitOpenCallback, + OnCircuitClosed: onCircuitClosedCallback, } -``` - -4. Update the status of the monitor based on the success or failure of events using the `UpdateMonitorStatusByName` function: -```go -success := true // Or false, depending on the event result -updatedMonitor, err := tripper.UpdateMonitorStatusByName("MyMonitor", success) +monitor, err := tripper.AddMonitor(monitorOptions) if err != nil { - // Handle error + fmt.Println("Failed to add monitor:", err) + return } ``` -5. Check if a circuit is open or closed using the `IsCircuitOpen` function: - +#### Monitor With Percentage ```go -isOpen, err := tripper.IsCircuitOpen("MyMonitor") -if err != nil { - // Handle error +//Adding a monitor that will trip the circuit if count of failure is more than 10% in 1 minute +//for a minimum of 100 count +monitorOptions := tripper.MonitorOptions{ + Name: "example-monitor", + Threshold: 10, + ThresholdType: tripper.ThresholdCount, + MinimumCount: 100, + IntervalInSeconds: 60, + OnCircuitOpen: onCircuitOpenCallback, + OnCircuitClosed: onCircuitClosedCallback, } -if isOpen { - // Circuit is open, take appropriate action -} else { - // Circuit is closed, continue normal operation -} -``` - -6. Retrieve a monitor by its name using the `GetMonitorByName` function: -```go -monitor, err := tripper.GetMonitorByName("MyMonitor") +monitor, err := tripper.AddMonitor(monitorOptions) if err != nil { - // Handle error + fmt.Println("Failed to add monitor:", err) + return } -// Use the monitor as needed ``` - -7. Get a map of all monitors using the `GetAllMonitors` function: - +#### Monitor with Callbacks ```go -monitors := tripper.GetAllMonitors() -for name, monitor := range monitors { - // Process each monitor +func onCircuitOpenCallback(x tripper.CallbackEvent){ + fmt.Println("Callback OPEN") + fmt.Println(x.FailureCount) + fmt.Println(x.SuccessCount) + fmt.Println(x.Timestamp) +} +func onCircuitClosedCallback(x tripper.CallbackEvent){ + fmt.Println("Callback Closed") + fmt.Println(x.FailureCount) + fmt.Println(x.SuccessCount) + fmt.Println(x.Timestamp) +} +monitorOptions := tripper.MonitorOptions{ + Name: "example-monitor", + Threshold: 10, + ThresholdType: tripper.ThresholdCount, + MinimumCount: 100, + IntervalInSeconds: 60, + OnCircuitOpen: onCircuitOpenCallback, + OnCircuitClosed: onCircuitClosedCallback, } -``` - - -## Constants - -- `ThresholdCount` represents a threshold type based on count. -- `ThresholdPercentage` represents a threshold type based on percentage. - -## Types -### Monitor +monitor, err := tripper.AddMonitor(monitorOptions) +if err != nil { + fmt.Println("Failed to add monitor:", err) + return +} +``` +### Monitor Options -The `Monitor` struct represents a monitoring entity that tracks the status of a circuit. It contains the following fields: +| Option | Description | Required | Type | +|---------------------|--------------------------------------------------------------|----------|------------| +| `Name` | The name of the monitor. | Required | `string` | +| `Threshold` | The threshold value for the monitor. | Required | `float32` | +| `ThresholdType` | The type of threshold (`ThresholdCount` or `ThresholdRatio`). | Required | `string` | +| `MinimumCount` | The minimum number of events required for monitoring. | Required | `int64` | +| `IntervalInSeconds` | The time interval for monitoring in seconds. | Required | `int` | +| `OnCircuitOpen` | Callback function called when the circuit opens. | Optional | `func()` | +| `OnCircuitClosed` | Callback function called when the circuit closes. | Optional | `func()` | -- `Name`: Name of the monitor. -- `CircuitOpen`: Indicates whether the circuit is open or closed. -- `LastCapturedAt`: Timestamp of the last captured event. -- `CircuitOpenedSince`: Timestamp when the circuit was opened. -- `FailureCount`: Number of failures recorded. -- `SuccessCount`: Number of successes recorded. -- `Threshold`: Threshold value for triggering circuit open. -- `ThresholdType`: Type of threshold (e.g., percentage, count). -- `MinimumCount`: Minimum number of events required for monitoring. -- `IntervalInSeconds`: Interval in seconds for monitoring (should be non-zero and a multiple of 60). +Circuits are reset after `IntervalInSeconds` -## Functions +### Getting a Monitor -### AddMonitor +To get a monitor from the Tripper, use the `GetMonitor` function: ```go -func AddMonitor(monitor Monitor) error +monitor, err := tripper.GetMonitor("example-monitor") +if err != nil { + fmt.Println("Monitor not found:", err) + return +} ``` -`AddMonitor` adds a new monitor to the circuits map. It checks if a monitor with the given name already exists. It also validates the threshold type and threshold value. If the minimum count or interval is invalid, it returns an error. If the interval is not a multiple of 60, it returns an error. +### Updating Monitor Status -### UpdateMonitorStatusByName +To update the status of a monitor based on the success of an event, use the `UpdateStatus` function: ```go -func UpdateMonitorStatusByName(name string, success bool) (*Monitor, error) +monitor.UpdateStatus(true) // Success event +monitor.UpdateStatus(false) // Failure event ``` -`UpdateMonitorStatusByName` updates the status of a monitor by its name. It takes the name of the monitor and a boolean indicating the success status. It returns the updated monitor and an error, if any. +### Checking Circuit Status -### IsCircuitOpen +To check if a circuit is open or closed, use the `IsCircuitOpen` function: ```go -func IsCircuitOpen(name string) (bool, error) +isOpen := monitor.IsCircuitOpen() +if isOpen { + fmt.Println("Circuit is open") +} else { + fmt.Println("Circuit is closed") +} ``` -`IsCircuitOpen` checks if a circuit with the given name is open or closed. It returns a boolean indicating whether the circuit is open or closed, and an error if the circuit does not exist. +### Getting All Monitors -### GetMonitorByName +To get all monitors in the Tripper, use the `GetAllMonitors` function: ```go -func GetMonitorByName(name string) (*Monitor, error) +monitors := tripper.GetAllMonitors() +for name, monitor := range monitors { + fmt.Println("Monitor:", name) + fmt.Println("Circuit Open:", monitor.IsCircuitOpen()) + fmt.Println("Success Count:", monitor.Get().SuccessCount) + fmt.Println("Failure Count:", monitor.Get().FailureCount) + fmt.Println() +} ``` -`GetMonitorByName` retrieves a monitor by its name. It returns the monitor if found, otherwise it returns an error. +### Example: HTTP Request with Circuit Breaker -### GetAllMonitors +Here's an example of using Tripper to handle HTTP requests with a circuit breaker: ```go -func GetAllMonitors() map[string]*Monitor +import ( + "fmt" + "net/http" + "time" + + "github.com/your-username/tripper" +) + +func main() { + // Create a Tripper instance + tripper := tripper.Configure(tripper.TripperOptions{}) + + // Add a monitor for the HTTP request circuit + monitorOptions := tripper.MonitorOptions{ + Name: "http-request-monitor", + Threshold: 10, + ThresholdType: tripper.ThresholdCount, + MinimumCount: 100, + IntervalInSeconds: 60, + OnCircuitOpen: func(t tripper.CallbackEvent) { + fmt.Println("Circuit is open for HTTP requests. Triggered at:", time.Unix(t.Timestamp, 0)) + }, + OnCircuitClosed: func(t tripper.CallbackEvent) { + fmt.Println("Circuit is closed for HTTP requests. Triggered at:", time.Unix(t.Timestamp, 0)) + }, + } + + _, err := tripper.AddMonitor(monitorOptions) + if err != nil { + fmt.Println("Failed to add monitor:", err) + return + } + + // Make an HTTP request with circuit breaker protection + client := http.Client{ + Transport: tripper.NewTransport(http.DefaultTransport), + } + + resp, err := client.Get("https://api.example.com/data") + if err != nil { + fmt.Println("HTTP request failed:", err) + return + } + defer resp.Body.Close() + + // Process the response + // ... + + fmt.Println("HTTP request successful!") +} ``` -`GetAllMonitors` returns a map of all monitors. +## License + +This project is licensed under the [MIT License](LICENSE). + \ No newline at end of file diff --git a/tripper.go b/tripper.go index b950049..b555e11 100644 --- a/tripper.go +++ b/tripper.go @@ -2,6 +2,7 @@ package tripper import ( "fmt" + "sync" "time" ) @@ -15,181 +16,238 @@ const ( var thresholdTypes = []string{ThresholdCount, ThresholdPercentage} // Monitor represents a monitoring entity that tracks the status of a circuit. -type Monitor struct { - Name string // Name of the monitor - CircuitOpen bool // Indicates whether the circuit is open or closed - LastCapturedAt int64 // Timestamp of the last captured event - CircuitOpenedSince int64 // Timestamp when the circuit was opened - FailureCount int64 // Number of failures recorded - SuccessCount int64 // Number of successes recorded - Threshold float32 // Threshold value for triggering circuit open - ThresholdType string // Type of threshold (e.g., percentage, count) - MinimumCount int64 // Minimum number of events required for monitoring - IntervalInSeconds int // Interval in seconds for monitoring (should be non-zero and multiple of 60) -} - -var circuits = make(map[string]*Monitor) - -// AddMonitor adds a new monitor to the circuits map. -// It checks if the monitor with the given name already exists. -// It also validates the threshold type and threshold value. -// If the minimum count or interval is invalid, it returns an error. -// If the interval is not a multiple of 60, it returns an error. -// Parameters: -// - monitor: The monitor to be added. -// -// Returns: -// - error: An error if the monitor is invalid or if a monitor with the same name already exists. -func AddMonitor(monitor Monitor) error { - if _, ok := circuits[monitor.Name]; ok { - return fmt.Errorf("Monitor with name %s already exists", monitor.Name) +type Monitor interface { + Get() *MonitorImplementation + UpdateStatus(success bool) + IsCircuitOpen() bool +} + +// TripperOptions represents options for configuring the Tripper. +type TripperOptions struct { +} + +// TripperImplementation represents the implementation of the Tripper interface. +type TripperImplementation struct { + Circuits map[string]Monitor + Options TripperOptions +} + +// Tripper represents a circuit breaker tripper. +type Tripper interface { + AddMonitor(monitorOptions MonitorOptions) (Monitor, error) + GetMonitor(name string) (Monitor, error) + GetAllMonitors() map[string]Monitor +} + +// MonitorOptions represents options for configuring a Monitor. +type MonitorOptions struct { + Name string // Name of the monitor + Threshold float32 // Threshold value for triggering circuit open + ThresholdType string // Type of threshold (e.g., percentage, count) + MinimumCount int64 // Minimum number of events required for monitoring + IntervalInSeconds int // Interval in seconds for monitoring (should be non-zero and multiple of 60) + OnCircuitOpen func(t CallbackEvent) + OnCircuitClosed func(t CallbackEvent) +} + +// MonitorImplementation represents the implementation of the Monitor interface. +type MonitorImplementation struct { + Options MonitorOptions + FailureCount int64 // Number of failures recorded + SuccessCount int64 // Number of successes recorded + CircuitOpen bool // Indicates whether the circuit is open or closed + LastCapturedAt int64 // Timestamp of the last captured event + CircuitOpenedSince int64 // Timestamp when the circuit was opened + Ticker *time.Ticker + Mutex sync.Mutex +} + +// CallbackEvent represents an event callback for the circuit monitor. +type CallbackEvent struct { + Timestamp int64 + SuccessCount int64 + FailureCount int64 +} + +// Configure configures the Tripper with the provided options. +func Configure(p TripperOptions) Tripper { + return &TripperImplementation{ + Options: p, + Circuits: make(map[string]Monitor), + } +} + +// ConfigureNewMonitor creates and configures a new Monitor with the provided options. +func ConfigureNewMonitor(p MonitorOptions) Monitor { + return &MonitorImplementation{ + Options: p, + } +} + +// Get returns the MonitorImplementation. +func (t *MonitorImplementation) Get() *MonitorImplementation { + return t +} + +// AddMonitor adds a new Monitor with the provided options. +func (t *TripperImplementation) AddMonitor(monitorOptions MonitorOptions) (Monitor, error) { + if _, ok := t.Circuits[monitorOptions.Name]; ok { + return nil, fmt.Errorf("Monitor with name %s already exists", monitorOptions.Name) } // check if the threshold type is valid validThresholdType := false for _, thType := range thresholdTypes { - if thType == monitor.ThresholdType { + if thType == monitorOptions.ThresholdType { validThresholdType = true break } } if !validThresholdType { - return fmt.Errorf("invalid threshold type %s", monitor.ThresholdType) + return nil, fmt.Errorf("invalid threshold type %s", monitorOptions.ThresholdType) } //if the threshold type is percentage, check if the threshold is between 0 and 100 - if monitor.ThresholdType == ThresholdPercentage && (monitor.Threshold < 0 || monitor.Threshold > 100) { - return fmt.Errorf("invalid threshold value %f for percentage type", monitor.Threshold) + if monitorOptions.ThresholdType == ThresholdPercentage && (monitorOptions.Threshold < 0 || monitorOptions.Threshold > 100) { + return nil, fmt.Errorf("invalid threshold value %f for percentage type", monitorOptions.Threshold) } // if the threshold type is count, check if the threshold is greater than 0 - if monitor.ThresholdType == ThresholdCount && monitor.Threshold <= 0 { - return fmt.Errorf("invalid threshold value %f for count type", monitor.Threshold) + if monitorOptions.ThresholdType == ThresholdCount && monitorOptions.Threshold <= 0 { + return nil, fmt.Errorf("invalid threshold value %f for count type", monitorOptions.Threshold) } // if the minimum count is less than 1, return an error - if monitor.MinimumCount < 1 { - return fmt.Errorf("invalid minimum count %d", monitor.MinimumCount) + if monitorOptions.MinimumCount < 1 { + return nil, fmt.Errorf("invalid minimum count %d", monitorOptions.MinimumCount) + } + + //if threshold is type count then minimum count should be greater than threshold + if monitorOptions.ThresholdType == ThresholdCount && monitorOptions.MinimumCount <= int64(monitorOptions.Threshold) { + return nil, fmt.Errorf("minimum count should be greater than threshold") } // if the interval is less than 60, return an error - if monitor.IntervalInSeconds < 60 { - return fmt.Errorf("invalid interval %d", monitor.IntervalInSeconds) + if monitorOptions.IntervalInSeconds < 60 { + return nil, fmt.Errorf("invalid interval %d", monitorOptions.IntervalInSeconds) } // if the interval is not a multiple of 60, return an error - if monitor.IntervalInSeconds%60 != 0 { - return fmt.Errorf("invalid interval %d", monitor.IntervalInSeconds) + if monitorOptions.IntervalInSeconds%60 != 0 { + return nil, fmt.Errorf("invalid interval %d", monitorOptions.IntervalInSeconds) } - circuits[monitor.Name] = &monitor - return nil -} + // configure the monitor + m := ConfigureNewMonitor(MonitorOptions{ + Name: monitorOptions.Name, + Threshold: monitorOptions.Threshold, + ThresholdType: monitorOptions.ThresholdType, + MinimumCount: monitorOptions.MinimumCount, + IntervalInSeconds: monitorOptions.IntervalInSeconds, + OnCircuitOpen: monitorOptions.OnCircuitOpen, + OnCircuitClosed: monitorOptions.OnCircuitClosed, + }) -// updateStatusByMonitor updates the status of a monitor based on the success or failure of a request. -// It takes a pointer to a Monitor struct and a boolean value indicating the success of the request. -// If the current timestamp is outside the monitor's interval, the success and failure counts are reset. -// The last captured timestamp is updated to the current timestamp. -// If the request is successful, the success count is incremented; otherwise, the failure count is incremented. -// The circuit open flag is set to false. -// If the total count of successes and failures is less than the minimum count, the function returns. -// If the threshold type is ThresholdCount, and the failure count is greater than the threshold, -// the circuit open flag is set to true and the circuit opened since timestamp is updated. -// If the threshold type is ThresholdPercentage, and the percentage of failures is greater than the threshold, -// the circuit open flag is set to true and the circuit opened since timestamp is updated. -func updateStatusByMonitor(monitor *Monitor, success bool) { + //start ticker - currentTs := getStartOfIntervalTimestamp(monitor) + t.Circuits[monitorOptions.Name] = m + return m, nil +} - //reset if not within a minute - if currentTs-monitor.LastCapturedAt >= int64(monitor.IntervalInSeconds) { - monitor.SuccessCount = 0 - monitor.FailureCount = 0 - monitor.CircuitOpenedSince = 0 +// GetMonitor returns the Monitor with the provided name. +func (t *TripperImplementation) GetMonitor(name string) (Monitor, error) { + monitor, ok := t.Circuits[name] + if !ok { + return nil, fmt.Errorf("Monitor with name %s does not exist", name) } + return monitor, nil +} - monitor.LastCapturedAt = currentTs - +// UpdateStatus updates the status of the Monitor based on the success of the event. +func (m *MonitorImplementation) UpdateStatus(success bool) { + //add a lock here + m.Mutex.Lock() + defer m.Mutex.Unlock() + + if m.Ticker == nil { + m.Ticker = time.NewTicker(time.Duration(m.Options.IntervalInSeconds) * time.Second) + go func() { + for range m.Ticker.C { + m.SuccessCount = 0 + m.FailureCount = 0 + m.CircuitOpenedSince = 0 + m.CircuitOpen = false + m.Options.OnCircuitClosed(CallbackEvent{ + Timestamp: getTimestamp(), + SuccessCount: m.SuccessCount, + FailureCount: m.FailureCount, + }) + } + }() + } + + m.LastCapturedAt = getTimestamp() if success { - monitor.SuccessCount++ + m.SuccessCount++ } else { - monitor.FailureCount++ + m.FailureCount++ } - - monitor.CircuitOpen = false - - //if not yet minimum retur - if monitor.SuccessCount+monitor.FailureCount < monitor.MinimumCount { + if m.SuccessCount+m.FailureCount < m.Options.MinimumCount { return } - - if monitor.ThresholdType == ThresholdCount { - if float32(monitor.FailureCount) > monitor.Threshold { - monitor.CircuitOpen = true - monitor.CircuitOpenedSince = monitor.LastCapturedAt + if m.Options.ThresholdType == ThresholdCount { + if float32(m.FailureCount) >= m.Options.Threshold { + m.CircuitOpen = true + m.CircuitOpenedSince = m.LastCapturedAt + } else { + m.CircuitOpen = false + m.CircuitOpenedSince = 0 } } else { // if the threshold type is percentage, check if the percentage of failures is greater than the threshold - totalRequests := monitor.FailureCount + monitor.SuccessCount - failurePercentage := (monitor.FailureCount * 100) / totalRequests - if float32(failurePercentage) > monitor.Threshold { - monitor.CircuitOpen = true - monitor.CircuitOpenedSince = monitor.LastCapturedAt + totalRequests := m.FailureCount + m.SuccessCount + failurePercentage := (m.FailureCount * 100) / totalRequests + if float32(failurePercentage) >= m.Options.Threshold { + m.CircuitOpen = true + m.CircuitOpenedSince = m.LastCapturedAt + } else { + m.CircuitOpen = false + m.CircuitOpenedSince = 0 } } -} + if m.CircuitOpen { -// UpdateMonitorStatusByName updates the status of a monitor by its name. -// It takes the name of the monitor and a boolean indicating the success status. -// It returns the updated monitor and an error, if any. -func UpdateMonitorStatusByName(name string, success bool) (*Monitor, error) { - monitor, err := GetMonitorByName(name) - if err != nil { - return nil, err - } - updateStatusByMonitor(monitor, success) - return monitor, nil -} + if m.Options.OnCircuitOpen != nil { -// IsCircuitOpen checks if a circuit with the given name is open or closed. -// It returns a boolean indicating whether the circuit is open or closed, and an error if the circuit does not exist. -func IsCircuitOpen(name string) (bool, error) { - monitor, ok := circuits[name] - if !ok { - return false, fmt.Errorf("Monitor with name %s does not exist", name) - } + m.Options.OnCircuitOpen(CallbackEvent{ + Timestamp: m.LastCapturedAt, + SuccessCount: m.SuccessCount, + FailureCount: m.FailureCount, + }) + } - now := getStartOfIntervalTimestamp(monitor) - if monitor.CircuitOpen && (now-monitor.CircuitOpenedSince >= int64(monitor.IntervalInSeconds)) { - monitor.CircuitOpen = false + } else { + if m.Options.OnCircuitClosed != nil { + m.Options.OnCircuitClosed(CallbackEvent{ + Timestamp: m.LastCapturedAt, + SuccessCount: m.SuccessCount, + FailureCount: m.FailureCount, + }) + } } - return monitor.CircuitOpen, nil + } -// GetMonitorByName retrieves a monitor by its name. -// It returns the monitor if found, otherwise it returns an error. -func GetMonitorByName(name string) (*Monitor, error) { - monitor, ok := circuits[name] - if !ok { - return nil, fmt.Errorf("Monitor with name %s does not exist", name) - } - return monitor, nil +// IsCircuitOpen returns true if the circuit is open, false otherwise. +func (m *MonitorImplementation) IsCircuitOpen() bool { + return m.CircuitOpen } -// GetAllMonitors returns a map of all monitors. -func GetAllMonitors() map[string]*Monitor { - return circuits +// GetAllMonitors returns all the monitors in the Tripper. +func (t *TripperImplementation) GetAllMonitors() map[string]Monitor { + return t.Circuits } -// getStartOfIntervalTimestamp calculates the start of the interval timestamp based on the current time and the monitor's interval. -// It rounds the current time to the nearest minute, subtracts the difference between the rounded time and the current time, -// and calculates the remainder when divided by the monitor's interval in seconds. It then returns the start of the interval timestamp. -func getStartOfIntervalTimestamp(monitor *Monitor) int64 { +// getTimestamp returns the current timestamp in Unix format. +func getTimestamp() int64 { currentTime := time.Now() - roundedTime := currentTime.Round(time.Minute).Unix() - diff := roundedTime - currentTime.Unix() - if diff > 0 { - roundedTime = roundedTime - 60 - } - remainder := roundedTime % int64(monitor.IntervalInSeconds) - startOfInterval := time.Unix(roundedTime-remainder, 0) - return startOfInterval.Unix() + return currentTime.Unix() } diff --git a/tripper_test.go b/tripper_test.go index 1e0f723..977dcfe 100644 --- a/tripper_test.go +++ b/tripper_test.go @@ -1,267 +1,383 @@ package tripper import ( + "sync" "testing" + "time" "github.com/stretchr/testify/assert" ) -func TestUpdateStatusByMonitor(t *testing.T) { - monitor := &Monitor{ - SuccessCount: 5, - FailureCount: 3, - CircuitOpen: false, - MinimumCount: 10, - ThresholdType: ThresholdCount, - Threshold: 3, - Name: "test", - IntervalInSeconds: 60, - } - - monitor.CircuitOpenedSince = getStartOfIntervalTimestamp(monitor) - 30 - monitor.LastCapturedAt = getStartOfIntervalTimestamp(monitor) - - // Test case 1: Success - updateStatusByMonitor(monitor, true) - assert.Equal(t, int64(6), monitor.SuccessCount) - assert.Equal(t, int64(3), monitor.FailureCount) - assert.False(t, monitor.CircuitOpen) - - // Test case 2: Failure - updateStatusByMonitor(monitor, false) - assert.Equal(t, int64(6), monitor.SuccessCount) - assert.Equal(t, int64(4), monitor.FailureCount) - assert.True(t, monitor.CircuitOpen) - - // Test case 3: Circuit opened since more than a minute, reset counts - monitor.LastCapturedAt = getStartOfIntervalTimestamp(monitor) - 120 - updateStatusByMonitor(monitor, true) - assert.Equal(t, int64(1), monitor.SuccessCount) - assert.Equal(t, int64(0), monitor.FailureCount) - assert.False(t, monitor.CircuitOpen) - assert.Equal(t, int64(0), monitor.CircuitOpenedSince) - - // Test case 4: Circuit opened since less than a minute, do not reset counts - monitor.CircuitOpenedSince = monitor.LastCapturedAt - 30 - updateStatusByMonitor(monitor, false) - assert.Equal(t, int64(1), monitor.SuccessCount) - assert.Equal(t, int64(1), monitor.FailureCount) - assert.False(t, monitor.CircuitOpen) - assert.NotEqual(t, int64(0), monitor.CircuitOpenedSince) - - // Test case 5: Circuit open threshold count reached - monitor.SuccessCount = 5 - monitor.FailureCount = 5 - monitor.ThresholdType = ThresholdCount - monitor.Threshold = 5 - updateStatusByMonitor(monitor, false) - assert.Equal(t, int64(5), monitor.SuccessCount) - assert.Equal(t, int64(6), monitor.FailureCount) - assert.True(t, monitor.CircuitOpen) - assert.Equal(t, monitor.LastCapturedAt, monitor.CircuitOpenedSince) - - // Test case 6: Circuit open threshold percentage reached - monitor.SuccessCount = 10 - monitor.FailureCount = 90 - monitor.ThresholdType = ThresholdPercentage - monitor.Threshold = 80 - updateStatusByMonitor(monitor, false) - assert.Equal(t, int64(10), monitor.SuccessCount) - assert.Equal(t, int64(91), monitor.FailureCount) - assert.True(t, monitor.CircuitOpen) - assert.Equal(t, monitor.LastCapturedAt, monitor.CircuitOpenedSince) -} func TestAddMonitor(t *testing.T) { // Test case 1: Add a new monitor successfully - monitor := Monitor{ - SuccessCount: 5, - FailureCount: 3, - CircuitOpen: false, - MinimumCount: 10, - ThresholdType: ThresholdCount, - Threshold: 3, - Name: "test", + // Expected output: No error + monitorOptions := MonitorOptions{ + Name: "test", + Threshold: 65, + MinimumCount: 20, + IntervalInSeconds: 120, + ThresholdType: ThresholdPercentage, } - monitor.IntervalInSeconds = 60 - monitor.CircuitOpenedSince = getStartOfIntervalTimestamp(&monitor) - 30 - monitor.LastCapturedAt = getStartOfIntervalTimestamp(&monitor) - - err := AddMonitor(monitor) + tripperOpts := TripperOptions{} + tripper := Configure(tripperOpts) + m, err := tripper.AddMonitor(monitorOptions) assert.NoError(t, err) - assert.NotNil(t, circuits["test"]) - assert.Equal(t, &monitor, circuits["test"]) + assert.NotNil(t, m) + r, errOk := tripper.GetMonitor("test") + assert.NoError(t, errOk) + assert.Equal(t, r, m) // Test case 2: Add a monitor with an existing name - err = AddMonitor(monitor) + // Expected output: An error + _, err = tripper.AddMonitor(monitorOptions) assert.Error(t, err) assert.EqualError(t, err, "Monitor with name test already exists") - assert.NotNil(t, circuits["test"]) - assert.Equal(t, &monitor, circuits["test"]) - // Test case 3: Add a monitor with an invalid threshold type - monitor.Name = "test1" - monitor.ThresholdType = "invalid" - err = AddMonitor(monitor) + // Expected output: An error + monitorOptions.ThresholdType = "invalid" + monitorOptions.Name = "invalid" + _, err = tripper.AddMonitor(monitorOptions) assert.Error(t, err) assert.EqualError(t, err, "invalid threshold type invalid") - assert.Nil(t, circuits["test1"]) - // Test case 4: Add a monitor with an invalid threshold value for percentage type - monitor.ThresholdType = ThresholdPercentage - monitor.Threshold = -10 - err = AddMonitor(monitor) + // Expected output: An error + monitorOptions.ThresholdType = ThresholdPercentage + monitorOptions.Threshold = -1 + _, err = tripper.AddMonitor(monitorOptions) assert.Error(t, err) - assert.EqualError(t, err, "invalid threshold value -10.000000 for percentage type") - assert.Nil(t, circuits["test1"]) - + assert.EqualError(t, err, "invalid threshold value -1.000000 for percentage type") // Test case 5: Add a monitor with an invalid threshold value for count type - monitor.ThresholdType = ThresholdCount - monitor.Threshold = 0 - err = AddMonitor(monitor) + // Expected output: An error + monitorOptions.ThresholdType = ThresholdCount + monitorOptions.Threshold = 0 + _, err = tripper.AddMonitor(monitorOptions) assert.Error(t, err) assert.EqualError(t, err, "invalid threshold value 0.000000 for count type") - assert.Nil(t, circuits["test1"]) - // Test case 6: Add a monitor with an invalid minimum count - monitor.MinimumCount = 0 - monitor.Threshold = 1 - err = AddMonitor(monitor) + // Expected output: An error + monitorOptions.ThresholdType = ThresholdPercentage + monitorOptions.Threshold = 65 + monitorOptions.MinimumCount = 0 + _, err = tripper.AddMonitor(monitorOptions) assert.Error(t, err) assert.EqualError(t, err, "invalid minimum count 0") - assert.Nil(t, circuits["test1"]) - // Test case 7: Add a monitor with an invalid interval - monitor.MinimumCount = 10 - monitor.IntervalInSeconds = 59 - err = AddMonitor(monitor) + // Expected output: An error + monitorOptions.MinimumCount = 20 + monitorOptions.IntervalInSeconds = 59 + _, err = tripper.AddMonitor(monitorOptions) assert.Error(t, err) assert.EqualError(t, err, "invalid interval 59") - assert.Nil(t, circuits["test1"]) - // Test case 8: Add a monitor with an interval that is not a multiple of 60 - monitor.IntervalInSeconds = 61 - err = AddMonitor(monitor) + // Expected output: An error + monitorOptions.IntervalInSeconds = 61 + _, err = tripper.AddMonitor(monitorOptions) assert.Error(t, err) assert.EqualError(t, err, "invalid interval 61") - assert.Nil(t, circuits["test1"]) -} -func TestUpdateMonitorStatusByName(t *testing.T) { - // Test case 1: Update monitor status successfully - monitor := &Monitor{ - SuccessCount: 5, - FailureCount: 3, - CircuitOpen: false, - MinimumCount: 10, - ThresholdType: ThresholdCount, - Threshold: 3, - Name: "test", - } - - monitor.IntervalInSeconds = 60 - monitor.CircuitOpenedSince = getStartOfIntervalTimestamp(monitor) - 30 - monitor.LastCapturedAt = getStartOfIntervalTimestamp(monitor) - circuits["test"] = monitor - updatedMonitor, err := UpdateMonitorStatusByName("test", true) + // Test case 9: Add a monitor with a valid threshold type and threshold value + // Expected output: No error + monitorOptions.IntervalInSeconds = 120 + monitorOptions.Name = "test2" + monitorOptions.ThresholdType = ThresholdCount + monitorOptions.Threshold = 10 + _, err = tripper.AddMonitor(monitorOptions) assert.NoError(t, err) - assert.Equal(t, int64(6), updatedMonitor.SuccessCount) - assert.Equal(t, int64(3), updatedMonitor.FailureCount) - assert.False(t, updatedMonitor.CircuitOpen) - // Test case 2: Update monitor status with non-existent name - _, err = UpdateMonitorStatusByName("nonexistent", true) + // Test case 10: Add a monitor with a threshold type count and minimum value less than threshold + // Expected output: An error + monitorOptions.MinimumCount = 5 + monitorOptions.Threshold = 6 + monitorOptions.Name = "test10" + _, err = tripper.AddMonitor(monitorOptions) assert.Error(t, err) - assert.EqualError(t, err, "Monitor with name nonexistent does not exist") + assert.EqualError(t, err, "minimum count should be greater than threshold") } -func TestIsCircuitOpen(t *testing.T) { - // Test case 1: Circuit is not open - monitor := &Monitor{ - CircuitOpen: false, - Name: "test", + +func TestUpdateStatus(t *testing.T) { + // Test case 1: Update status with success=true + // Expected output: Success count incremented + callBackCalledOpen := false + callBackCalledClosed := false + monitorOptions := MonitorOptions{ + Name: "test", + Threshold: 50, + MinimumCount: 4, + IntervalInSeconds: 60, + ThresholdType: ThresholdPercentage, + OnCircuitOpen: func(x CallbackEvent) { + callBackCalledOpen = true + }, + OnCircuitClosed: func(x CallbackEvent) { + callBackCalledClosed = true + }, } + tripperOpts := TripperOptions{} + tripper := Configure(tripperOpts) + m, err := tripper.AddMonitor(monitorOptions) + assert.NoError(t, err) + assert.NotNil(t, m) + + m.UpdateStatus(true) + assert.Equal(t, int64(1), m.Get().SuccessCount) + assert.Equal(t, int64(0), m.Get().FailureCount) + + // Test case 2: Update status with success=false + // Expected output: Failure count incremented + m.UpdateStatus(false) + assert.Equal(t, int64(1), m.Get().SuccessCount) + assert.Equal(t, int64(1), m.Get().FailureCount) + + // Test case 3: Update status with success=true and minimum count not reached + // Expected output: Success count incremented, circuit not opened + + m.UpdateStatus(true) + assert.Equal(t, int64(2), m.Get().SuccessCount) + assert.Equal(t, int64(1), m.Get().FailureCount) + assert.False(t, m.Get().CircuitOpen) + + // Test case 4: Update status with success=false and minimum count not reached + // Expected output: Failure count incremented, circuit not opened + m.UpdateStatus(false) + assert.Equal(t, int64(2), m.Get().SuccessCount) + assert.Equal(t, int64(2), m.Get().FailureCount) + assert.True(t, m.Get().CircuitOpen) + assert.True(t, callBackCalledOpen) + assert.NotZero(t, m.Get().CircuitOpenedSince) + + // Test case 5: Update status with success=false and failure count greater than threshold (ThresholdType=ThresholdCount) + // Expected output: Failure count incremented, circuit opened + m.UpdateStatus(false) + assert.Equal(t, int64(2), m.Get().SuccessCount) + assert.Equal(t, int64(3), m.Get().FailureCount) + assert.True(t, m.Get().CircuitOpen) + assert.NotZero(t, m.Get().CircuitOpenedSince) + + m.UpdateStatus(true) + m.UpdateStatus(true) + m.UpdateStatus(true) + m.UpdateStatus(true) + m.UpdateStatus(true) + assert.False(t, m.Get().CircuitOpen) + assert.True(t, callBackCalledClosed) + + // Test case 6: Update status with success=false and failure percentage greater than threshold (ThresholdType=ThresholdPercentage) + // Expected output: Failure count incremented, circuit opened + m.Get().SuccessCount = 0 + m.Get().FailureCount = 0 + m.Get().Options.ThresholdType = ThresholdCount + m.Get().Options.Threshold = 2 //two failures would trigger the circuit + m.Get().Options.MinimumCount = 2 + m.Get().CircuitOpen = false + m.Get().CircuitOpenedSince = 0 - monitor.IntervalInSeconds = 60 - monitor.CircuitOpenedSince = getStartOfIntervalTimestamp(monitor) - 30 - monitor.LastCapturedAt = getStartOfIntervalTimestamp(monitor) - circuits["test"] = monitor - isOpen, err := IsCircuitOpen("test") + m.UpdateStatus(false) + assert.Equal(t, int64(0), m.Get().SuccessCount) + assert.Equal(t, int64(1), m.Get().FailureCount) + assert.False(t, m.Get().CircuitOpen) + + m.UpdateStatus(false) + m.UpdateStatus(false) + m.UpdateStatus(true) + assert.Equal(t, int64(1), m.Get().SuccessCount) + assert.Equal(t, int64(3), m.Get().FailureCount) + assert.True(t, m.Get().CircuitOpen) + assert.NotZero(t, m.Get().CircuitOpenedSince) + + m.Get().Options.Threshold = 20 + m.UpdateStatus(true) + assert.False(t, m.Get().CircuitOpen) + assert.True(t, callBackCalledClosed) +} + +func TestInterval(t *testing.T) { + callBackCalledClosed := false + monitorOptionsX := MonitorOptions{ + Name: "x", + Threshold: 2, + MinimumCount: 4, + IntervalInSeconds: 60, + ThresholdType: ThresholdCount, + OnCircuitClosed: func(x CallbackEvent) { + callBackCalledClosed = true + }, + } + tripperOptsX := TripperOptions{} + tripperX := Configure(tripperOptsX) + mx, err := tripperX.AddMonitor(monitorOptionsX) assert.NoError(t, err) - assert.False(t, isOpen) + mx.Get().Options.IntervalInSeconds = 1 + mx.UpdateStatus(false) + mx.UpdateStatus(false) + mx.UpdateStatus(true) + mx.UpdateStatus(false) + assert.Equal(t, int64(1), mx.Get().SuccessCount) + assert.Equal(t, int64(3), mx.Get().FailureCount) + assert.True(t, mx.Get().CircuitOpen) + assert.NotZero(t, mx.Get().CircuitOpenedSince) + time.Sleep(1100 * time.Millisecond) + assert.Equal(t, int64(0), mx.Get().SuccessCount) + assert.Equal(t, int64(0), mx.Get().FailureCount) + assert.False(t, mx.Get().CircuitOpen) + assert.True(t, callBackCalledClosed) + assert.Zero(t, mx.Get().CircuitOpenedSince) +} - // Test case 2: Circuit is open, but less than a minute has passed - monitor.CircuitOpen = true - isOpen, err = IsCircuitOpen("test") +func TestUpdateStatusRaceSingle(t *testing.T) { + monitorOptionsX := MonitorOptions{ + Name: "x", + Threshold: 2, + MinimumCount: 4, + IntervalInSeconds: 60, + ThresholdType: ThresholdCount, + } + tripperOptsX := TripperOptions{} + tripperX := Configure(tripperOptsX) + _, err := tripperX.AddMonitor(monitorOptionsX) assert.NoError(t, err) - assert.True(t, isOpen) + mx, _ := tripperX.GetMonitor("x") + numGoroutines := 100 + var wg sync.WaitGroup + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func() { + + mx.UpdateStatus((mx.Get().SuccessCount+mx.Get().FailureCount)%2 == 0) + // Decrement the wait group counter + wg.Done() + }() + } + wg.Wait() + assert.Equal(t, int64(100), mx.Get().SuccessCount+mx.Get().FailureCount) +} - // Test case 3: Circuit is open, and more than a minute has passed - monitor.CircuitOpenedSince = getStartOfIntervalTimestamp(monitor) - 70 - isOpen, err = IsCircuitOpen("test") +func TestUpdateStatusRaceTwo(t *testing.T) { + monitorOptionsX := MonitorOptions{ + Name: "x", + Threshold: 2, + MinimumCount: 4, + IntervalInSeconds: 60, + ThresholdType: ThresholdCount, + } + tripperOptsX := TripperOptions{} + tripperX := Configure(tripperOptsX) + _, err := tripperX.AddMonitor(monitorOptionsX) assert.NoError(t, err) - assert.False(t, isOpen) + mx, _ := tripperX.GetMonitor("x") + + monitorOptionsX1 := MonitorOptions{ + Name: "x1", + Threshold: 2, + MinimumCount: 4, + IntervalInSeconds: 60, + ThresholdType: ThresholdCount, + } + + _, errx1 := tripperX.AddMonitor(monitorOptionsX1) + assert.NoError(t, errx1) + mx1, _ := tripperX.GetMonitor("x1") + + numGoroutines := 100 + status := false + var wg sync.WaitGroup + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func() { + + status = !status + mx.UpdateStatus(status) + mx1.UpdateStatus(status) + wg.Done() + }() + } + wg.Wait() + assert.Equal(t, int64(200), mx.Get().SuccessCount+mx.Get().FailureCount+mx1.Get().SuccessCount+mx1.Get().FailureCount) - // Test case 4: Circuit does not exist - isOpen, err = IsCircuitOpen("nonexistent") - assert.Error(t, err) - assert.EqualError(t, err, "Monitor with name nonexistent does not exist") - assert.False(t, isOpen) } -func TestGetMonitorByName(t *testing.T) { - // Test case 1: Get existing monitor by name - monitor := &Monitor{ - SuccessCount: 5, - FailureCount: 3, - CircuitOpen: false, - MinimumCount: 10, - ThresholdType: ThresholdCount, - Threshold: 3, - Name: "test", +func TestGetMonitor(t *testing.T) { + tripperOpts := TripperOptions{} + tripper := Configure(tripperOpts) + + // Test case 1: Get an existing monitor + // Expected output: Monitor and no error + monitorOptions := MonitorOptions{ + Name: "test", + Threshold: 65, + MinimumCount: 20, + IntervalInSeconds: 120, + ThresholdType: ThresholdPercentage, } - monitor.IntervalInSeconds = 60 - monitor.CircuitOpenedSince = getStartOfIntervalTimestamp(monitor) - 30 - monitor.LastCapturedAt = getStartOfIntervalTimestamp(monitor) - circuits["test"] = monitor + _, err := tripper.AddMonitor(monitorOptions) + assert.NoError(t, err) - result, err := GetMonitorByName("test") + m, err := tripper.GetMonitor("test") assert.NoError(t, err) - assert.Equal(t, monitor, result) + assert.NotNil(t, m) - // Test case 2: Get non-existent monitor by name - result, err = GetMonitorByName("nonexistent") + // Test case 2: Get a non-existing monitor + // Expected output: Error + _, err = tripper.GetMonitor("non-existing") assert.Error(t, err) - assert.EqualError(t, err, "Monitor with name nonexistent does not exist") - assert.Nil(t, result) + assert.EqualError(t, err, "Monitor with name non-existing does not exist") +} +func TestIsCircuitOpen(t *testing.T) { + // Test case 1: Circuit is closed + // Expected output: false + monitorOptions := MonitorOptions{ + Name: "test", + Threshold: 65, + MinimumCount: 20, + IntervalInSeconds: 120, + ThresholdType: ThresholdPercentage, + } + tripperOpts := TripperOptions{} + tripper := Configure(tripperOpts) + m, err := tripper.AddMonitor(monitorOptions) + assert.NoError(t, err) + assert.NotNil(t, m) + + result := m.IsCircuitOpen() + assert.False(t, result) + + // Test case 2: Circuit is open + // Expected output: true + m.Get().CircuitOpen = true + result = m.IsCircuitOpen() + assert.True(t, result) } func TestGetAllMonitors(t *testing.T) { - // Test case 1: Get all monitors successfully - monitor1 := &Monitor{ - SuccessCount: 5, - FailureCount: 3, - CircuitOpen: false, - MinimumCount: 10, - ThresholdType: ThresholdCount, - Threshold: 3, + tripperOpts := TripperOptions{} + tripper := Configure(tripperOpts) + + // Test case 1: Get all monitors when there are no monitors + // Expected output: Empty map + monitors := tripper.GetAllMonitors() + assert.Empty(t, monitors) + + // Test case 2: Get all monitors when there are multiple monitors + // Expected output: Map with all monitors + monitorOptions1 := MonitorOptions{ Name: "test1", - IntervalInSeconds: 60, - } - monitor2 := &Monitor{ - SuccessCount: 10, - FailureCount: 5, - CircuitOpen: true, + Threshold: 65, MinimumCount: 20, + IntervalInSeconds: 120, ThresholdType: ThresholdPercentage, - Threshold: 80, + } + monitorOptions2 := MonitorOptions{ Name: "test2", - IntervalInSeconds: 60, + Threshold: 70, + MinimumCount: 30, + IntervalInSeconds: 180, + ThresholdType: ThresholdPercentage, } + _, err1 := tripper.AddMonitor(monitorOptions1) + assert.NoError(t, err1) + _, err2 := tripper.AddMonitor(monitorOptions2) + assert.NoError(t, err2) - //reset circuits to empty - circuits = make(map[string]*Monitor) - - circuits["test1"] = monitor1 - circuits["test2"] = monitor2 - - result := GetAllMonitors() - assert.Equal(t, 2, len(result)) - assert.Equal(t, monitor1, result["test1"]) - assert.Equal(t, monitor2, result["test2"]) + monitors = tripper.GetAllMonitors() + assert.Len(t, monitors, 2) + assert.Contains(t, monitors, "test1") + assert.Contains(t, monitors, "test2") }