Skip to content

Commit ba0db78

Browse files
committed
issue #171: working solution for Schrödinger's bug
1 parent 1399445 commit ba0db78

File tree

5 files changed

+248
-123
lines changed

5 files changed

+248
-123
lines changed

Makefile

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# sourced by https://github.com/octomation/makefiles
22

3-
.DEFAULT_GOAL = test-with-coverage
3+
.DEFAULT_GOAL = check
44
GIT_HOOKS = post-merge pre-commit pre-push
55
GO_VERSIONS = 1.11 1.12 1.13 1.14 1.15 1.16
66
GO111MODULE = on
@@ -291,10 +291,13 @@ endif
291291

292292
export PATH := $(GOBIN):$(PATH)
293293

294-
init: deps test lint hooks
294+
init: deps check hooks
295295
$(AT) git config core.autocrlf input
296296
.PHONY: init
297297

298+
check: test lint
299+
.PHONY: check
300+
298301
clean: deps-clean test-clean
299302
.PHONY: clean
300303

@@ -315,7 +318,7 @@ format: go-fmt
315318
generate: go-generate format
316319
.PHONY: generate
317320

318-
refresh: deps-tidy update deps generate test
321+
refresh: deps-tidy update deps generate check
319322
.PHONY: refresh
320323

321324
update: deps-update tools-update
@@ -324,5 +327,5 @@ update: deps-update tools-update
324327
verbose: make-verbose go-verbose
325328
.PHONY: verbose
326329

327-
verify: deps-check generate test lint git-check
330+
verify: deps-check generate check git-check
328331
.PHONY: verify

context.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package retry
2+
3+
import "context"
4+
5+
type lite struct {
6+
context.Context
7+
signal <-chan struct{}
8+
}
9+
10+
func (ctx lite) Done() <-chan struct{} {
11+
return ctx.signal
12+
}
13+
14+
func (ctx lite) Err() error {
15+
select {
16+
case <-ctx.signal:
17+
return context.Canceled
18+
default:
19+
return nil
20+
}
21+
}
22+
23+
// equal to go.octolab.org/errors.Unwrap
24+
func unwrap(err error) error {
25+
// compatible with github.com/pkg/errors
26+
type causer interface {
27+
Cause() error
28+
}
29+
// compatible with built-in errors since 1.13
30+
type wrapper interface {
31+
Unwrap() error
32+
}
33+
34+
for err != nil {
35+
layer, is := err.(wrapper)
36+
if is {
37+
err = layer.Unwrap()
38+
continue
39+
}
40+
cause, is := err.(causer)
41+
if is {
42+
err = cause.Cause()
43+
continue
44+
}
45+
break
46+
}
47+
return err
48+
}

context_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package retry
2+
3+
import (
4+
"context"
5+
"runtime"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestContext(t *testing.T) {
11+
t.Run("with cancel", func(t *testing.T) {
12+
t.Parallel()
13+
14+
var (
15+
sig = make(chan struct{})
16+
ctx = context.Context(lite{context.TODO(), sig})
17+
)
18+
if ctx.Err() != nil {
19+
t.Error("invalid state")
20+
}
21+
22+
ctx, cancel := context.WithCancel(ctx)
23+
if ctx.Err() != nil {
24+
t.Error("invalid state")
25+
}
26+
27+
verify(t, ctx, cancel, sig)
28+
})
29+
30+
t.Run("with deadline", func(t *testing.T) {
31+
t.Parallel()
32+
33+
var (
34+
sig = make(chan struct{})
35+
ctx = context.Context(lite{context.TODO(), sig})
36+
)
37+
if ctx.Err() != nil {
38+
t.Error("invalid state")
39+
}
40+
41+
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Hour))
42+
if ctx.Err() != nil {
43+
t.Error("invalid state")
44+
}
45+
46+
verify(t, ctx, cancel, sig)
47+
})
48+
49+
t.Run("with timeout", func(t *testing.T) {
50+
t.Parallel()
51+
52+
var (
53+
sig = make(chan struct{})
54+
ctx = context.Context(lite{context.TODO(), sig})
55+
)
56+
if ctx.Err() != nil {
57+
t.Error("invalid state")
58+
}
59+
60+
ctx, cancel := context.WithTimeout(ctx, time.Hour)
61+
if ctx.Err() != nil {
62+
t.Error("invalid state")
63+
}
64+
65+
verify(t, ctx, cancel, sig)
66+
})
67+
68+
t.Run("with value", func(t *testing.T) {
69+
t.Parallel()
70+
71+
var (
72+
sig = make(chan struct{})
73+
ctx = context.Context(lite{context.TODO(), sig})
74+
)
75+
if ctx.Err() != nil {
76+
t.Error("invalid state")
77+
}
78+
79+
ctx = context.WithValue(ctx, key{}, "value")
80+
if expected, obtained := "value", ctx.Value(key{}); obtained != expected {
81+
t.Errorf("expected: %q, obtained: %q", expected, obtained)
82+
}
83+
84+
close(sig)
85+
})
86+
}
87+
88+
// helpers
89+
90+
func stop(timer *time.Timer) {
91+
if !timer.Stop() {
92+
select {
93+
case <-timer.C:
94+
default:
95+
}
96+
}
97+
}
98+
99+
func verify(t *testing.T, ctx context.Context, cancel context.CancelFunc, sig chan struct{}) {
100+
t.Helper()
101+
102+
timer := time.NewTimer(schedule)
103+
close(sig)
104+
select {
105+
case <-timer.C:
106+
t.Error("invalid state")
107+
case <-ctx.Done():
108+
if ctx.Err() == nil {
109+
t.Error("invalid state")
110+
}
111+
}
112+
113+
stop(timer)
114+
cancel()
115+
}
116+
117+
type key struct{}
118+
119+
var schedule = 10 * time.Duration(runtime.NumCPU()) * time.Millisecond

retry.go

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,30 @@ func Do(
4545
err error = Error("have no any try")
4646
clean error
4747
)
48+
4849
ctx, is := breaker.(context.Context)
4950
if !is {
50-
ctx = context.Background()
51+
ctx = lite{context.Background(), breaker.Done()}
5152
}
52-
ctx, cancel := context.WithCancel(ctx)
53+
5354
for attempt, should := uint(0), true; should; attempt++ {
5455
clean = unwrap(err)
5556
for i, repeat := 0, len(strategies); should && i < repeat; i++ {
5657
should = should && strategies[i](breaker, attempt, clean)
5758
}
59+
5860
select {
5961
case <-breaker.Done():
60-
cancel()
6162
return breaker.Err()
6263
default:
6364
if should {
6465
err = action(ctx)
6566
}
6667
}
68+
6769
should = should && err != nil
6870
}
69-
cancel()
71+
7072
return err
7173
}
7274

@@ -103,30 +105,3 @@ func Go(
103105
return err
104106
}
105107
}
106-
107-
// equal to go.octolab.org/errors.Unwrap
108-
func unwrap(err error) error {
109-
// compatible with github.com/pkg/errors
110-
type causer interface {
111-
Cause() error
112-
}
113-
// compatible with built-in errors since 1.13
114-
type wrapper interface {
115-
Unwrap() error
116-
}
117-
118-
for err != nil {
119-
layer, is := err.(wrapper)
120-
if is {
121-
err = layer.Unwrap()
122-
continue
123-
}
124-
cause, is := err.(causer)
125-
if is {
126-
err = cause.Cause()
127-
continue
128-
}
129-
break
130-
}
131-
return err
132-
}

0 commit comments

Comments
 (0)