Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions breaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ type SlidingWindowBreaker struct {

// State

currentStart atomic.Int64 // in unix microseconds
currentStart atomic.Int64 // monotonic nanoseconds since start (see nowNanos)
currentSuccessCount atomic.Int64
currentFailureCount atomic.Int64
lastSuccessCount atomic.Int64
Expand Down Expand Up @@ -184,9 +184,9 @@ func (s *SlidingWindowBreaker) observe(halfOpen, failure bool) stateChange {
return stateChangeClose
}

currentStartMicros := s.currentStart.Load()
sinceStart := sinceMicros(currentStartMicros)
firstCallInNewWindow := s.currentStart.CompareAndSwap(currentStartMicros, time.Now().UnixMicro())
currentStartNanos := s.currentStart.Load()
sinceStart := sinceNanos(currentStartNanos)
firstCallInNewWindow := s.currentStart.CompareAndSwap(currentStartNanos, nowNanos())

Comment thread
costela marked this conversation as resolved.
// The second condition ensures only one goroutine can swap the windows. Necessary since multiple swaps would
// overwrite the last counts to some near zero value.
Expand Down Expand Up @@ -234,10 +234,3 @@ func (s *SlidingWindowBreaker) apply(o *options) error {

return nil
}

func sinceMicros(micros int64) time.Duration {
if micros == 0 {
return 0
}
return time.Since(time.UnixMicro(micros))
}
28 changes: 24 additions & 4 deletions hoglet.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Circuit struct {

// State

openedAt atomic.Int64 // unix microseconds
openedAt atomic.Int64 // monotonic nanoseconds since start (see [nowNanos]); 0 = closed
}

// options is a sub-struct to avoid requiring type parameters in the [Option] type.
Expand Down Expand Up @@ -123,7 +123,7 @@ func (c *Circuit) State() State {
return StateClosed
}

if c.halfOpenDelay == 0 || time.Since(time.UnixMicro(oa)) < c.halfOpenDelay {
if c.halfOpenDelay == 0 || sinceNanos(oa) < c.halfOpenDelay {
// open
return StateOpen
}
Expand Down Expand Up @@ -151,19 +151,39 @@ func (c *Circuit) stateForCall() State {
// It is safe for concurrent calls and only the first one will actually set opening time.
func (c *Circuit) open() {
// CompareAndSwap is needed to avoid clobbering another goroutine's openedAt value.
c.openedAt.CompareAndSwap(0, time.Now().UnixMicro())
c.openedAt.CompareAndSwap(0, nowNanos())
}

// reopen forcefully (re)marks the circuit as open, resetting the half-open time.
func (c *Circuit) reopen() {
c.openedAt.Store(time.Now().UnixMicro())
c.openedAt.Store(nowNanos())
}

// close closes the circuit.
func (c *Circuit) close() {
c.openedAt.Store(0)
}

// start is captured once at package load so the circuit and breakers can measure elapsed time using the monotonic
// clock, making time-based state transitions immune to wall-clock jumps (e.g. NTP steps). Timestamps derived from it
// (e.g. [Circuit.openedAt]) are stored as monotonic nanoseconds since start and are not wall-clock meaningful.
var start = time.Now()

// nowNanos returns the monotonic nanoseconds elapsed since package load. It is always > 0 for any real call (start is
// captured before any circuit can be used), so 0 remains usable as an "unset"/closed sentinel.
func nowNanos() int64 {
return int64(time.Since(start))
}

// sinceNanos returns the duration elapsed since the given monotonic-nanos timestamp (as produced by [nowNanos]).
// A zero timestamp is treated as "unset" and yields a zero duration.
func sinceNanos(nanos int64) time.Duration {
if nanos == 0 {
return 0
}
return time.Since(start) - time.Duration(nanos)
}
Comment thread
costela marked this conversation as resolved.
Outdated

// ObserverForCall returns an [Observer] for the incoming call.
// It is called exactly once per call to [Circuit.Call], before calling the wrapped function.
// If the breaker is open, it returns [ErrCircuitOpen] as an error and a nil [Observer].
Expand Down
4 changes: 2 additions & 2 deletions hoglet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ func TestHoglet_Do(t *testing.T) {
require.NoError(t, err)
for i, call := range tt.calls {
if call.halfOpen {
// simulate passage of time
h.openedAt.Store(int64(time.Now().Add(-h.halfOpenDelay).UnixMicro()))
// simulate passage of time: mark the circuit as opened halfOpenDelay ago
h.openedAt.Store(nowNanos() - int64(h.halfOpenDelay))
Comment thread
costela marked this conversation as resolved.
}

var err error
Expand Down
Loading