Atlas › Test

TestClockedRateLimiter_WaitN_NoRecycle

Exact test identity: go.temporal.io/server/common/quotas/TestClockedRateLimiter_WaitN_NoRecycle

Package
go.temporal.io/server/common/quotas
Suite / test hierarchy
TestClockedRateLimiter_WaitN_NoRecycle
Test
TestClockedRateLimiter_WaitN_NoRecycle
Introduced at
clocked_rate_limiter.go ×1 Frontier kind: Joint frontier
Covered ranges
30
Covered lines
98
Covered files
2

Covered source

Expand a file to inspect source; the > gutter marks covered lines.

go.temporal.io/server/common/quotas/clocked_rate_limiter.go 50 covered LOC · 18 ranges

Open complete file

25 )
26
27 > func NewClockedRateLimiter(rateLimiter *rate.Limiter, timeSource clock.TimeSource) ClockedRateLimiter { clocked_rate_limiter.go
28 > return ClockedRateLimiter{
29 > rateLimiter: rateLimiter,
30 > timeSource: timeSource,
31 > recycleCh: make(chan struct{}),
32 > }
33 > }
34
35 func (l ClockedRateLimiter) Allow() bool {
48 }
49
50 > func (r ClockedReservation) OK() bool { clocked_rate_limiter.go
51 > return r.reservation.OK()
52 > }
53
54 > func (r ClockedReservation) Delay() time.Duration { clocked_rate_limiter.go
55 > return r.DelayFrom(r.timeSource.Now())
56 > }
57
58 > func (r ClockedReservation) DelayFrom(t time.Time) time.Duration { clocked_rate_limiter.go
59 > return r.reservation.DelayFrom(t)
60 > }
61
62 > func (r ClockedReservation) Cancel() { clocked_rate_limiter.go
63 > r.CancelAt(r.timeSource.Now())
64 > }
65
66 > func (r ClockedReservation) CancelAt(t time.Time) { clocked_rate_limiter.go
67 > r.reservation.CancelAt(t)
68 > }
69
70 func (l ClockedRateLimiter) Reserve() ClockedReservation {
77 }
78
79 > func (l ClockedRateLimiter) Wait(ctx context.Context) error { clocked_rate_limiter.go
80 > return l.WaitN(ctx, 1)
81 > }
82
83 // WaitN is the only method that is different from rate.Limiter. We need to fully reimplement this method because
84 // the original method uses time.Now(), and does not allow us to pass in a time.Time. Fortunately, it can be built on
85 // top of ReserveN. However, there are some optimizations that we can make.
86 > func (l ClockedRateLimiter) WaitN(ctx context.Context, token int) error { clocked_rate_limiter.go
87 > reservation := ClockedReservation{l.rateLimiter.ReserveN(l.timeSource.Now(), token), l.timeSource}
88 > if !reservation.OK() {
89 return fmt.Errorf("%w: WaitN(n=%d)", ErrRateLimiterReservationCannotBeMade, token)
90 }
91
92 > waitDuration := reservation.Delay() clocked_rate_limiter.go
93 >
94 > // Optimization: if the waitDuration is 0, we don't need to start a timer.
95 > if waitDuration <= 0 {
96 > return nil clocked_rate_limiter.go
97 > }
98
99 // Optimization: if the waitDuration is longer than the context deadline, we can immediately return an error.
100 > if deadline, ok := ctx.Deadline(); ok { clocked_rate_limiter.go
101 if l.timeSource.Now().Add(waitDuration).After(deadline) {
102 reservation.Cancel()
105 }
106
107 > waitExpired := make(chan struct{}) clocked_rate_limiter.go
108 > timer := l.timeSource.AfterFunc(waitDuration, func() {
109 close(waitExpired)
110 })
111 > defer timer.Stop() clocked_rate_limiter.go
112 >
113 > for {
114 > select {
115 > case <-ctx.Done(): clocked_rate_limiter.go
116 > reservation.Cancel()
117 > return fmt.Errorf("%w: %v", ErrRateLimiterWaitInterrupted, ctx.Err())
118 case <-waitExpired:
119 return nil
120 > case <-l.recycleCh: clocked_rate_limiter.go
121 > if token > 1 {
122 > break // recycling 1 token to a process requesting >1 tokens is a no-op, because we only know that at least one token was not used clocked_rate_limiter.go
123 }
124
163 // so that the actual rate of completed actions is as close to the intended rate limit as possible.
164 // If no process is waiting for a token when RecycleToken is called, this is a no-op.
165 > func (l ClockedRateLimiter) RecycleToken() { clocked_rate_limiter.go
166 > select {
167 > case l.recycleCh <- struct{}{}: clocked_rate_limiter.go
168 default:
169 }
go.temporal.io/server/common/clock/event_time_source.go 48 covered LOC · 12 ranges

Open complete file

39
40 // NewEventTimeSource returns a EventTimeSource with the current time set to Unix zero: 1970-01-01 00:00:00 +0000 UTC.
41 > func NewEventTimeSource() *EventTimeSource { event_time_source.go
42 > return &EventTimeSource{
43 > now: time.Unix(0, 0),
44 > }
45 > }
46
47 // Some clients depend on the fact that the runtime's timers do _not_ run synchronously.
55
56 // Now return the current time.
57 > func (ts *EventTimeSource) Now() time.Time { event_time_source.go
58 > ts.mu.RLock()
59 > defer ts.mu.RUnlock()
60 >
61 > return ts.now
62 > }
63
64 func (ts *EventTimeSource) Since(t time.Time) time.Duration {
71 // wrap all such calls in a goroutine. If the duration is non-positive, the callback will fire immediately before
72 // AfterFunc returns.
73 > func (ts *EventTimeSource) AfterFunc(d time.Duration, f func()) Timer { event_time_source.go
74 > if d < 0 {
75 d = 0
76 }
77 > timer := &fakeTimer{timeSource: ts, deadline: ts.Now().Add(d), callback: f} event_time_source.go
78 > ts.addTimer(timer)
79 > return timer
80 }
81
96 }
97
98 > func (ts *EventTimeSource) addTimer(t *fakeTimer) { event_time_source.go
99 > ts.mu.Lock()
100 > defer ts.mu.Unlock()
101 > t.index = len(ts.timers)
102 > ts.timers = append(ts.timers, t)
103 > ts.fireTimers()
104 > }
105
106 // Update the fake current time. It returns the timeSource so that you can chain calls like this:
200
201 // Stop the timer. Returns true if the timer was active.
202 > func (t *fakeTimer) Stop() bool { event_time_source.go
203 > t.timeSource.mu.Lock()
204 > defer t.timeSource.mu.Unlock()
205 >
206 > if t.done {
207 return false
208 }
209
210 > i := t.index event_time_source.go
211 > timers := t.timeSource.timers
212 >
213 > timers[i] = timers[len(timers)-1] // swap with last timer
214 > timers[i].index = i // update index of swapped timer
215 > timers = timers[:len(timers)-1] // shrink list
216 >
217 > t.timeSource.timers = timers
218 > t.done = true // ensure that the timer is not reused
219 >
220 > return true
221 }