@@ -3,17 +3,29 @@ package limiter
3
3
import (
4
4
"context"
5
5
"fmt"
6
+ "github.com/platinummonkey/go-concurrency-limits/limit"
6
7
"sync"
8
+ "time"
7
9
8
10
"github.com/platinummonkey/go-concurrency-limits/core"
9
11
)
10
12
13
+ const longBlockingTimeout = time .Hour * 24 * 30 * 12 * 100 // 100 years
14
+
11
15
// BlockingListener wraps the wrapped Limiter's Listener to correctly handle releasing blocked connections
12
16
type BlockingListener struct {
13
17
delegateListener core.Listener
14
18
c * sync.Cond
15
19
}
16
20
21
+ func NewBlockingListener (delegateListener core.Listener ) * BlockingListener {
22
+ mu := sync.Mutex {}
23
+ return & BlockingListener {
24
+ delegateListener : delegateListener ,
25
+ c : sync .NewCond (& mu ),
26
+ }
27
+ }
28
+
17
29
func (l * BlockingListener ) unblock () {
18
30
l .c .Broadcast ()
19
31
}
@@ -40,38 +52,129 @@ func (l *BlockingListener) OnSuccess() {
40
52
l .unblock ()
41
53
}
42
54
55
+ // timeoutWaiter will wait for a timeout or unblock signal
56
+ type timeoutWaiter struct {
57
+ timeoutSig chan struct {}
58
+ closerSig chan struct {}
59
+ c * sync.Cond
60
+ once sync.Once
61
+ timeout time.Duration
62
+ }
63
+
64
+ func newTimeoutWaiter (c * sync.Cond , timeout time.Duration ) * timeoutWaiter {
65
+ return & timeoutWaiter {
66
+ timeoutSig : make (chan struct {}),
67
+ closerSig : make (chan struct {}),
68
+ c : c ,
69
+ timeout : timeout ,
70
+ }
71
+ }
72
+
73
+ func (w * timeoutWaiter ) start () {
74
+ // start two routines, one runner to signal, another blocking to wait and call unblock
75
+ go func () {
76
+ w .run ()
77
+ }()
78
+ go func () {
79
+ w .c .L .Lock ()
80
+ defer w .c .L .Unlock ()
81
+ w .c .Wait ()
82
+ w .unblock ()
83
+ }()
84
+ }
85
+
86
+ func (w * timeoutWaiter ) run () {
87
+ select {
88
+ case <- w .closerSig :
89
+ close (w .timeoutSig )
90
+ return
91
+ case <- time .After (w .timeout ):
92
+ // call unblock
93
+ close (w .timeoutSig )
94
+ return
95
+ }
96
+ }
97
+
98
+ func (w * timeoutWaiter ) unblock () {
99
+ w .once .Do (func () {
100
+ close (w .closerSig )
101
+ })
102
+ }
103
+
104
+ // wait blocks until we've timed out
105
+ func (w * timeoutWaiter ) wait () <- chan struct {} {
106
+ return w .timeoutSig
107
+ }
108
+
43
109
// BlockingLimiter implements a Limiter that blocks the caller when the limit has been reached. The caller is
44
110
// blocked until the limiter has been released. This limiter is commonly used in batch clients that use the limiter
45
111
// as a back-pressure mechanism.
46
112
type BlockingLimiter struct {
113
+ logger limit.Logger
47
114
delegate core.Limiter
48
115
c * sync.Cond
116
+ timeout time.Duration
49
117
}
50
118
51
119
// NewBlockingLimiter will create a new blocking limiter
52
120
func NewBlockingLimiter (
53
121
delegate core.Limiter ,
122
+ timeout time.Duration ,
123
+ logger limit.Logger ,
54
124
) * BlockingLimiter {
55
125
mu := sync.Mutex {}
126
+ if timeout <= 0 {
127
+ timeout = longBlockingTimeout
128
+ }
129
+ if logger == nil {
130
+ logger = limit.NoopLimitLogger {}
131
+ }
56
132
return & BlockingLimiter {
133
+ logger : logger ,
57
134
delegate : delegate ,
58
135
c : sync .NewCond (& mu ),
136
+ timeout : timeout ,
59
137
}
60
138
}
61
139
62
140
// tryAcquire will block when attempting to acquire a token
63
- func (l * BlockingLimiter ) tryAcquire (ctx context.Context ) core.Listener {
141
+ func (l * BlockingLimiter ) tryAcquire (ctx context.Context ) ( core.Listener , bool ) {
64
142
l .c .L .Lock ()
65
143
defer l .c .L .Unlock ()
66
144
for {
145
+ // if the deadline has passed, fail quickly
146
+ deadline , deadlineSet := ctx .Deadline ()
147
+ if deadlineSet && time .Now ().UTC ().After (deadline ) {
148
+ l .logger .Debugf ("deadline passed ctx=%v" , time .Now ().UTC ().After (deadline ), ctx )
149
+ return nil , false
150
+ }
151
+
67
152
// try to acquire a new token and return immediately if successful
68
153
listener , ok := l .delegate .Acquire (ctx )
69
154
if ok && listener != nil {
70
- return listener
155
+ l .logger .Debugf ("delegate returned a listener ctx=%v" , ctx )
156
+ return listener , true
71
157
}
72
158
73
159
// We have reached the limit so block until a token is released
74
- l .c .Wait ()
160
+ timeout := l .timeout // the default if not set
161
+
162
+ // infer timeout from deadline if set.
163
+ if deadlineSet {
164
+ timeout := deadline .Sub (time .Now ().UTC ())
165
+ // if the deadline has passed, return acquire failure
166
+ if timeout <= 0 {
167
+ l .logger .Debugf ("deadline passed ctx=%v" , ctx )
168
+ return nil , false
169
+ }
170
+ }
171
+
172
+ // block until we timeout
173
+ timeoutWaiter := newTimeoutWaiter (l .c , timeout )
174
+ timeoutWaiter .start ()
175
+ l .logger .Debugf ("Blocking waiting for release or timeout ctx=%v" , ctx )
176
+ <- timeoutWaiter .wait ()
177
+ l .logger .Debugf ("blocking released, trying again to acquire ctx=%v" , ctx )
75
178
}
76
179
}
77
180
@@ -81,10 +184,12 @@ func (l *BlockingLimiter) tryAcquire(ctx context.Context) core.Listener {
81
184
//
82
185
// context Context for the request. The context is used by advanced strategies such as LookupPartitionStrategy.
83
186
func (l * BlockingLimiter ) Acquire (ctx context.Context ) (core.Listener , bool ) {
84
- delegateListener := l .tryAcquire (ctx )
85
- if delegateListener == nil {
187
+ delegateListener , ok := l .tryAcquire (ctx )
188
+ if ! ok && delegateListener == nil {
189
+ l .logger .Debugf ("did not acquire ctx=%v" , ctx )
86
190
return nil , false
87
191
}
192
+ l .logger .Debugf ("acquired, returning listener ctx=%v" , ctx )
88
193
return & BlockingListener {
89
194
delegateListener : delegateListener ,
90
195
c : l .c ,
0 commit comments