-
Notifications
You must be signed in to change notification settings - Fork 0
fix: PDX-2459 avoid parallel cache-misses on the same token #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+496
−31
Merged
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
f889fda
add a ticketlock
arnecls 3f68477
fix: wait for parallel requests
arnecls 4efa6f3
refetch after lock
arnecls ba30e1d
add retry-after header when timing out
arnecls 0000f96
explain granularity
arnecls 27d7dcb
Apply suggestions from code review
arnecls 4f6f962
improve lock handling
arnecls eda3a2a
make sure lock returns 0 after Close() has been called
arnecls 915174d
fix pause ticker contention
arnecls 8595e69
fix several issues on the use of inflight locks
arnecls 50497a1
fix/improve some outdated documentation
arnecls abc83fb
improve variable naming for clarity
arnecls ad763d4
Update internal/shared/ticketlock_test.go
arnecls cd88bcb
add concurrency test for ticket locks
arnecls 27cf679
add concurrent cancel test
arnecls 4211718
add lock behavior test
arnecls 942c8c0
fix deadlock on context cancel while waiting
arnecls a4f8355
add robustness to unlock
arnecls b0a2625
defensive granulatrity check
arnecls 9a346d2
Update internal/shared/ticketlock_test.go
arnecls File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package shared | ||
|
|
||
| // HeapUint64 implements a min-heap of uint64 values on top of a slice. | ||
| // Use this type with the container/heap package. | ||
| type HeapUint64 []uint64 | ||
|
|
||
| // Len returns the number of elements in the heap. | ||
| func (h HeapUint64) Len() int { | ||
| return len(h) | ||
| } | ||
|
|
||
| // Less returns true if the element at index i is less than the element at index j. | ||
| func (h HeapUint64) Less(i, j int) bool { return h[i] < h[j] } | ||
|
|
||
| // Swap swaps the elements at index i and j. | ||
| func (h HeapUint64) Swap(i, j int) { h[i], h[j] = h[j], h[i] } | ||
|
|
||
| // Push adds a new element to the heap. | ||
| // Use heap.Push(h, x) instead of this function. | ||
| func (h *HeapUint64) Push(x any) { | ||
| *h = append(*h, x.(uint64)) | ||
| } | ||
|
|
||
| // Peek returns the smallest element from the heap without removing it. | ||
| // It returns false if the heap is empty. | ||
| func (h *HeapUint64) Peek() (uint64, bool) { | ||
| if len(*h) == 0 { | ||
| return 0, false | ||
| } | ||
| return (*h)[0], true | ||
| } | ||
|
|
||
| // Pop removes and returns the smallest element from the heap. | ||
| // Use heap.Pop(h) instead of this function. | ||
| func (h *HeapUint64) Pop() any { | ||
| old := *h | ||
| n := len(old) | ||
| x := old[n-1] | ||
| *h = old[0 : n-1] | ||
| return x | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| package shared | ||
|
|
||
| import ( | ||
| "container/heap" | ||
| "context" | ||
| "sync" | ||
| "sync/atomic" | ||
| "time" | ||
| ) | ||
|
|
||
| type TicketLock struct { | ||
| nextTicket uint64 | ||
| activeTicket uint64 | ||
| pauseDuration time.Duration | ||
| canceledTickets *HeapUint64 | ||
| ticketGuard *sync.Mutex | ||
| } | ||
|
|
||
| // NewTicketLock creates a new ticket lock with the given granularity. | ||
| // The granularity is the time to wait between each lock acquisition check. | ||
| // The granularity should be small enough to not block the main thread for too | ||
| // long, but large enough to not waste too much time. | ||
| // A granularity of 5-10 milliseconds is a good starting point. | ||
| func NewTicketLock(granularity time.Duration) *TicketLock { | ||
| return &TicketLock{ | ||
| nextTicket: 1, | ||
| activeTicket: 1, | ||
| pauseDuration: granularity, | ||
| canceledTickets: &HeapUint64{}, | ||
| ticketGuard: &sync.Mutex{}, | ||
| } | ||
| } | ||
arnecls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // IsLocked returns true if the lock is currently held by a thread. | ||
| // Please note that this status can change right after the call to IsLocked(). | ||
| // I.e. this is not a reliable way to check if the lock is currently held by a | ||
| // thread. It is only meant for debugging purposes. | ||
| func (l *TicketLock) IsLocked() bool { | ||
| return atomic.LoadUint64(&l.activeTicket) != atomic.LoadUint64(&l.nextTicket) | ||
| } | ||
|
|
||
| // Lock tries to acquire a lock in a FIFO way. | ||
| func (l *TicketLock) Lock() uint64 { | ||
| return l.LockWithContext(context.Background()) | ||
| } | ||
|
|
||
| // LockWithContext tries to acquire a lock in a FIFO way. | ||
| // It returns 0 when the lock failed to be acquired due to a context | ||
| // cancellation or a timeout. | ||
| // If the lock was acquired, it returns the ticket number of the lock. | ||
| func (l *TicketLock) LockWithContext(ctx context.Context) uint64 { | ||
| ticket := atomic.AddUint64(&l.nextTicket, 1) - 1 | ||
| var pause *time.Ticker | ||
|
|
||
| for { | ||
| if atomic.LoadUint64(&l.activeTicket) == ticket { | ||
| return ticket | ||
| } | ||
|
|
||
| // Do a lazy initialization of the ticker to avoid creating a ticker if | ||
| // it is not needed. | ||
| if pause == nil { | ||
| pause = time.NewTicker(l.pauseDuration) | ||
| defer pause.Stop() | ||
| } | ||
|
|
||
| select { | ||
| // We use a ticker to yield the CPU during waiting and to be able to | ||
| // check on the context while pausing. | ||
| case <-pause.C: | ||
| continue | ||
arnecls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| case <-ctx.Done(): | ||
| // We need to keep track of canceled tickets as tickets are linearly | ||
| // ordered. If we don't do this, we cannot properly unlock the lock | ||
| // in the correct order. | ||
| l.ticketGuard.Lock() | ||
| defer l.ticketGuard.Unlock() | ||
| heap.Push(l.canceledTickets, ticket) | ||
arnecls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return 0 | ||
| } | ||
| } | ||
arnecls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Unlock releases the lock. | ||
| func (l *TicketLock) Unlock() { | ||
| l.ticketGuard.Lock() | ||
| defer l.ticketGuard.Unlock() | ||
|
|
||
| for { | ||
| ticket := atomic.AddUint64(&l.activeTicket, 1) | ||
| nextCanceledTicket, hasCanceledTickets := l.canceledTickets.Peek() | ||
|
|
||
| switch { | ||
| // No canceled tickets, we can return | ||
| case !hasCanceledTickets: | ||
| return | ||
|
|
||
| // The last canceled ticket is the same as the current ticket. | ||
| // We need to try again with the next ticket (which might also be | ||
| // canceled). | ||
| case nextCanceledTicket == ticket: | ||
| heap.Pop(l.canceledTickets) | ||
|
|
||
| // There are canceled tickets, but the current ticket is smaller than | ||
| // the first canceled ticket. | ||
| default: | ||
| return | ||
arnecls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package shared | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestTicketLock(t *testing.T) { | ||
arnecls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| assert := assert.New(t) | ||
|
|
||
| lock := NewTicketLock(time.Millisecond) | ||
arnecls marked this conversation as resolved.
Show resolved
Hide resolved
arnecls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ticket1 := lock.Lock() | ||
| assert.NotZero(ticket1, "Lock should return a non-zero ticket") | ||
| assert.Equal(uint64(1), ticket1, "Lock should return the first ticket") | ||
| assert.True(lock.IsLocked(), "Lock should return a non-zero ticket") | ||
|
|
||
| // Test timeout | ||
| ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) | ||
| defer cancel() | ||
arnecls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ticket2 := lock.LockWithContext(ctx) | ||
| assert.Zero(ticket2, "LockWithContext should return a zero ticket as it timed out before the lock was acquired") | ||
|
|
||
| // Test release | ||
| lock.Unlock() | ||
| assert.False(lock.IsLocked(), "Lock should return a zero ticket after the lock was released") | ||
|
|
||
| // Test if release properly increments the active ticket | ||
| ticket3 := lock.Lock() | ||
| assert.NotZero(ticket3, "Lock should return a non-zero ticket after the previous lock was released") | ||
| assert.NotEqual(ticket1, ticket3, "Lock should return a different ticket after the previous lock was released") | ||
| assert.Equal(uint64(3), ticket3, "Lock should return the third ticket, as the second lock was aborted") | ||
|
|
||
| // Test if release properly increments the active ticket with consecutive discards | ||
| ticket4 := lock.LockWithContext(ctx) | ||
| assert.Zero(ticket4, "LockWithContext should return a zero ticket as it timed out before the lock was acquired") | ||
| ticket5 := lock.LockWithContext(ctx) | ||
arnecls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| assert.Zero(ticket5, "LockWithContext should return a zero ticket as it timed out before the lock was acquired") | ||
|
|
||
| lock.Unlock() | ||
| ticket6 := lock.Lock() | ||
| assert.NotZero(ticket6, "Lock should return a non-zero ticket after the previous lock was released") | ||
| assert.NotEqual(ticket3, ticket6, "Lock should return a different ticket after the previous lock was released") | ||
| assert.Equal(uint64(6), ticket6, "Lock should return the sixth ticket, as the fourth and fifth locks were aborted") | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.