Skip to content

Commit

Permalink
Merge pull request #5 from kinfinity/feat/timeout
Browse files Browse the repository at this point in the history
Timeout Pattern
  • Loading branch information
kokou-egbewatt authored Mar 20, 2024
2 parents ebff600 + 7c08ed6 commit 2df3b40
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: BUILD & TESTS
name: Build & Tests
on:
workflow_dispatch:
pull_request:
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# **Resilience in Distributed Systems**

[![BUILD & TESTS](https://github.com/kinfinity/distributed-resilience/actions/workflows/build.yaml/badge.svg)](https://github.com/kinfinity/distributed-resilience/actions/workflows/build.yaml)
<table><tbody><tr><td><a href="https://github.com/kinfinity/distributed-resilience/actions/workflows/build.yaml"><img src="https://github.com/kinfinity/distributed-resilience/actions/workflows/build.yaml/badge.svg" alt="Build &amp; Tests"></a></td><td><a href="https://github.com/kinfinity/distributed-resilience/actions/workflows/codeql.yaml"><img src="https://github.com/kinfinity/distributed-resilience/actions/workflows/codeql.yaml/badge.svg" alt="CodeQL"></a></td></tr></tbody></table>

Distributed Systems need to be able to gracefully handle failures and recover from them. This is achieved through resilience, which involves designing systems while anticipating scenarios where nodes/services/resources over which the system is distributed may fail to be accessed or behave unexpected due to

Expand All @@ -14,9 +14,12 @@ Distributed Systems need to be able to gracefully handle failures and recover fr

This repository covers several patterns implemented in Golang which have been designed to handle resilience in distributed environments.

- [Timeout](./timeout/readme.md)
- [Retry](./retry/readme.md)
- [Circuit Breaker](./circuitbreaker/readme.md)

# **References**

- [Release It! Second Edition - Stability Patterns by Michael Nygard](https://pragprog.com/titles/mnee2/release-it-second-edition/)
- [codecentric resilience-design-patterns](https://www.codecentric.de/wissens-hub/blog/resilience-design-patterns-retry-fallback-timeout-circuit-breaker)
- [The Resilience Patterns your Microservices Teams Should Know by Victor Rentea](https://youtu.be/IR89tmg9v3A?si=w96y4S6AbVt_CviB)
3 changes: 1 addition & 2 deletions circuitbreaker/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,4 @@ func main() {

# **References:**

- **Michael Nygard**  https://pragprog.com/titles/mnee2/release-it-second-edition/ Second Edition - Stability Patterns
- https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
- [microsoft architecture patterns circuit breaker](https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker)
2 changes: 1 addition & 1 deletion retry/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ func main() {

# **References**

- https://learn.microsoft.com/en-us/azure/architecture/patterns/retry
- [microsoft architecture patterns retry](https://learn.microsoft.com/en-us/azure/architecture/patterns/retry)
37 changes: 37 additions & 0 deletions timeout/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Timeout Pattern in Go

The Timeout pattern is used to limit the execution time of a function or operation. It ensures that the operation completes within a specified time duration, and if it exceeds that duration, it either returns a default value or invokes a fallback function.

## Usage

```go
package main

import (
"context"
"fmt"
"time"

"github.com/kinfinity/distributed-resilience/timeout"
)

func main() {
// Create a Timeout instance with a fallback function
timeout := timeout.NewTimeOutWithFallback(5 * time.Second, func() error {
// Custom fallback logic here
return nil
})

// Watch for timeout
result := timeout.Watch(executionCompletionChan)
if result.result {
// Operation completed within the timeout duration
} else {
// Operation timed out
}
}
```

# **References**

- [ All you need to know about timeouts - Zalando ](https://engineering.zalando.com/posts/2023/07/all-you-need-to-know-about-timeouts.html)
73 changes: 73 additions & 0 deletions timeout/timeout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package timeout

import (
"context"
"fmt"
"time"
)

// Timeout
type TimeOut struct {
duration time.Duration
fallback func() error
}

// Returns nil if the operation times out
type TimeOutResult struct {
result bool
}

// TimeOut
func NewTimeOut(Delay time.Duration) *TimeOut {
return &TimeOut{
duration: Delay,
}
}

// TimeOut with options via
// fallback() error
func NewTimeOutWithFallback(Delay time.Duration, options ...interface{}) *TimeOut {

timeout := &TimeOut{
duration: Delay,
}

for _, option := range options {
switch opt := option.(type) {
case func() error:
timeout.fallback = opt
default:
panic(fmt.Sprintf("Unknown option type: %T", opt))
}
}

return timeout
}

// Creates a context with the duration lifetime
// func executes till the end| Context Lifetime expires
func (to *TimeOut) Watch(executionCompletionChan chan bool) *TimeOutResult {
// Build Context Lifetime
ctx, cancel := context.WithTimeout(context.Background(), to.duration)
defer cancel()

for {
select {
case result := <-executionCompletionChan:
return &TimeOutResult{
result: result,
}
case <-ctx.Done():
if to.fallback != nil {
to.fallback() // fallback on timeout
}
// Timeout
return &TimeOutResult{}
default:
}
// random delay before we check conditions again
// give timeout grace
time.Sleep(to.duration / 4)
}

}
74 changes: 74 additions & 0 deletions timeout/timeout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package timeout

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

// Mock function
type MockFunction struct {
mock.Mock
}

func (m *MockFunction) Execute() error {
args := m.Called()
return args.Error(0)
}

func simulateTimeOut(completionChan chan<- bool) {
time.Sleep(10 * time.Second)
completionChan <- true
}

// TestTimeOutWait
func TestTimeOutWait(t *testing.T) {
//
compChan := make(chan bool)
go func() { simulateTimeOut(compChan) }()

t.Run("TimeOut", func(t *testing.T) {
time_delay := time.Duration(8 * time.Second)
mockFn := new(MockFunction)
mockFn.On("Fallback").Return(nil)

timeout := NewTimeOut(time_delay)
response := timeout.Watch(compChan)

assert.Empty(t, response.result)
})

t.Run("Completion", func(t *testing.T) {
time_delay := time.Duration(12 * time.Second)
mockFn := new(MockFunction)
mockFn.On("Fallback").Return(nil)

timeout := NewTimeOut(time_delay)
response := timeout.Watch(compChan)

assert.NotEmpty(t, response.result)
})

}

// TestTimeOutWait
func TestTimeOutWaitFallback(t *testing.T) {
//
time_delay := time.Duration(4 * time.Second)
compChan := make(chan bool)
go func() { simulateTimeOut(compChan) }()

t.Run("FallbackExecutes", func(t *testing.T) {
mockFn := new(MockFunction)
mockFn.On("Execute").Return(nil).Times(int(1))

timeout := NewTimeOutWithFallback(time_delay, mockFn.Execute)
response := timeout.Watch(compChan)

assert.Empty(t, response.result)
mockFn.AssertNumberOfCalls(t, "Execute", int(1))
})

}

0 comments on commit 2df3b40

Please sign in to comment.