Skip to content

Commit 8349347

Browse files
authored
Simplify PublicationAwaiter (#718)
Make the `PublicationAwaiter` more concise by using a `sync.Cond` so that calls to `Await()` are notified to check and release themselves when an updated checkpoint is seen, rather than relying on managing many channels.
1 parent 1a4c5af commit 8349347

2 files changed

Lines changed: 74 additions & 118 deletions

File tree

await.go

Lines changed: 40 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,10 @@ package tessera
1717
import (
1818
"context"
1919
"errors"
20-
"fmt"
2120
"os"
2221
"sync"
2322
"time"
2423

25-
"container/list"
26-
2724
"github.com/transparency-dev/tessera/internal/parse"
2825
"k8s.io/klog/v2"
2926
)
@@ -33,7 +30,7 @@ import (
3330
// to fetch checkpoints using the `readCheckpoint` function.
3431
func NewPublicationAwaiter(ctx context.Context, readCheckpoint func(ctx context.Context) ([]byte, error), pollPeriod time.Duration) *PublicationAwaiter {
3532
a := &PublicationAwaiter{
36-
waiters: list.New(),
33+
c: sync.NewCond(&sync.Mutex{}),
3734
}
3835
go a.pollLoop(ctx, readCheckpoint, pollPeriod)
3936
return a
@@ -54,24 +51,13 @@ func NewPublicationAwaiter(ctx context.Context, readCheckpoint func(ctx context.
5451
// When used this way, it requires very little code at the point of use to
5552
// block until the new leaf is integrated into the tree.
5653
type PublicationAwaiter struct {
57-
// This mutex is used to protect all reads and writes to fields below.
58-
mu sync.Mutex
59-
// waiters is a linked list of type `waiter` and corresponds to all of the
60-
// client threads that are currently blocked on this awaiter. The list is
61-
// not sorted; new clients are simply added to the end. This can be changed
62-
// if this is the wrong decision, but the reasoning behind this decision is
63-
// that it is O(1) at the point of adding an entry to simply put it on the
64-
// end, and O(N) each time we poll to check all clients. Keeping the list
65-
// sorted by index would reduce the worst case in the poll loop, but increase
66-
// the cost at the point of adding a new entry.
67-
//
68-
// Once a waiter is added to this list, it belongs to the awaiter and the
69-
// awaiter takes sole responsibility for closing the channel in the waiter.
70-
waiters *list.List
71-
// size and checkpoint keep track of the latest seen size and checkpoint as
72-
// an optimization where the last seen value was already large enough.
54+
c *sync.Cond
55+
56+
// size, checkpoint, and err keep track of the latest size and checkpoint
57+
// (or error) seen by the poller.
7358
size uint64
7459
checkpoint []byte
60+
err error
7561
}
7662

7763
// Await blocks until the IndexFuture is resolved, and this new index has been
@@ -83,15 +69,24 @@ type PublicationAwaiter struct {
8369
// or in the event that there is an error getting a valid checkpoint, an error
8470
// will be returned from this method.
8571
func (a *PublicationAwaiter) Await(ctx context.Context, future IndexFuture) (Index, []byte, error) {
86-
ctx, span := tracer.Start(ctx, "tessera.Await")
72+
_, span := tracer.Start(ctx, "tessera.Await")
8773
defer span.End()
8874

8975
i, err := future()
9076
if err != nil {
9177
return i, nil, err
9278
}
93-
cp, err := a.await(ctx, i.Index)
94-
return i, cp, err
79+
80+
a.c.L.Lock()
81+
defer a.c.L.Unlock()
82+
for (a.size <= i.Index && a.err == nil) && ctx.Err() == nil {
83+
a.c.Wait()
84+
}
85+
// Ensure we propogate context done error, if any.
86+
if err := ctx.Err(); err != nil {
87+
a.err = err
88+
}
89+
return i, a.checkpoint, a.err
9590
}
9691

9792
// pollLoop MUST be called in a goroutine when constructing an PublicationAwaiter
@@ -100,101 +95,36 @@ func (a *PublicationAwaiter) Await(ctx context.Context, future IndexFuture) (Ind
10095
// the latest checkpoint from the log, parses the tree size, and releases all clients
10196
// that were blocked on an index smaller than this tree size.
10297
func (a *PublicationAwaiter) pollLoop(ctx context.Context, readCheckpoint func(ctx context.Context) ([]byte, error), pollPeriod time.Duration) {
103-
for {
98+
var (
99+
cp []byte
100+
cpErr error
101+
cpSize uint64
102+
)
103+
for done := false; !done; {
104104
select {
105105
case <-ctx.Done():
106106
klog.Info("PublicationAwaiter exiting due to context completion")
107-
return
107+
cp, cpSize, cpErr = nil, 0, ctx.Err()
108+
done = true
108109
case <-time.After(pollPeriod):
109-
// It's worth this small lock contention to make sure that no unnecessary
110-
// work happens in personalities that aren't performing writes.
111-
a.mu.Lock()
112-
hasClients := a.waiters.Front() != nil
113-
a.mu.Unlock()
114-
if !hasClients {
110+
cp, cpErr = readCheckpoint(ctx)
111+
switch {
112+
case errors.Is(cpErr, os.ErrNotExist):
115113
continue
114+
case cpErr != nil:
115+
cpSize = 0
116+
default:
117+
_, cpSize, _, cpErr = parse.CheckpointUnsafe(cp)
116118
}
117119
}
120+
121+
a.c.L.Lock()
118122
// Note that for now, this releases all clients in the event of a single failure.
119123
// If this causes problems, this could be changed to attempt retries.
120-
rawCp, err := readCheckpoint(ctx)
121-
if err != nil && !errors.Is(err, os.ErrNotExist) {
122-
a.releaseClientsErr(fmt.Errorf("readCheckpoint: %v", err))
123-
continue
124-
}
125-
_, size, _, err := parse.CheckpointUnsafe(rawCp)
126-
if err != nil {
127-
a.releaseClientsErr(err)
128-
continue
129-
}
130-
a.releaseClients(size, rawCp)
131-
}
132-
}
133-
134-
func (a *PublicationAwaiter) await(ctx context.Context, i uint64) ([]byte, error) {
135-
a.mu.Lock()
136-
if a.size > i {
137-
cp := a.checkpoint
138-
a.mu.Unlock()
139-
return cp, nil
140-
}
141-
done := make(chan checkpointOrDeath, 1)
142-
w := waiter{
143-
index: i,
144-
result: done,
145-
}
146-
a.waiters.PushBack(w)
147-
a.mu.Unlock()
148-
select {
149-
case <-ctx.Done():
150-
return nil, ctx.Err()
151-
case cod := <-done:
152-
return cod.cp, cod.err
124+
a.checkpoint = cp
125+
a.size = cpSize
126+
a.err = cpErr
127+
a.c.Broadcast()
128+
a.c.L.Unlock()
153129
}
154130
}
155-
156-
func (a *PublicationAwaiter) releaseClientsErr(err error) {
157-
cod := checkpointOrDeath{
158-
err: err,
159-
}
160-
a.mu.Lock()
161-
defer a.mu.Unlock()
162-
for e := a.waiters.Front(); e != nil; e = e.Next() {
163-
w := e.Value.(waiter)
164-
w.result <- cod
165-
close(w.result)
166-
}
167-
// Clear out the list now we've released everyone
168-
a.waiters.Init()
169-
}
170-
171-
func (a *PublicationAwaiter) releaseClients(size uint64, cp []byte) {
172-
cod := checkpointOrDeath{
173-
cp: cp,
174-
}
175-
a.mu.Lock()
176-
defer a.mu.Unlock()
177-
a.size = size
178-
a.checkpoint = cp
179-
for e := a.waiters.Front(); e != nil; e = e.Next() {
180-
w := e.Value.(waiter)
181-
if w.index < size {
182-
w.result <- cod
183-
close(w.result)
184-
// Need to do this removal after the loop has been fully iterated
185-
// It still needs to happen inside the mutex, but defers happen in
186-
// reverse order.
187-
defer a.waiters.Remove(e)
188-
}
189-
}
190-
}
191-
192-
type waiter struct {
193-
index uint64
194-
result chan checkpointOrDeath
195-
}
196-
197-
type checkpointOrDeath struct {
198-
cp []byte
199-
err error
200-
}

await_test.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,21 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package tessera_test
15+
package tessera
1616

1717
import (
1818
"bytes"
1919
"context"
2020
"crypto/sha256"
2121
"fmt"
2222
"sync"
23+
"sync/atomic"
2324
"time"
2425

2526
"errors"
2627
"testing"
2728

2829
"github.com/transparency-dev/formats/log"
29-
"github.com/transparency-dev/tessera"
3030
"golang.org/x/mod/sumdb/note"
3131
)
3232

@@ -136,11 +136,11 @@ func TestAwait(t *testing.T) {
136136
<-time.After(tC.cpDelay)
137137
return tC.cpBody, tC.cpErr
138138
}
139-
awaiter := tessera.NewPublicationAwaiter(ctx, readCheckpoint, 10*time.Millisecond)
139+
awaiter := NewPublicationAwaiter(ctx, readCheckpoint, 10*time.Millisecond)
140140

141-
future := func() (tessera.Index, error) {
141+
future := func() (Index, error) {
142142
<-time.After(tC.fDelay)
143-
return tessera.Index{Index: tC.fIndex}, tC.fErr
143+
return Index{Index: tC.fIndex}, tC.fErr
144144
}
145145
i, cp, err := awaiter.Await(ctx, future)
146146
if gotErr := err != nil; gotErr != tC.wantErr {
@@ -195,14 +195,14 @@ func TestAwait_multiClient(t *testing.T) {
195195
}
196196
return n, nil
197197
}
198-
awaiter := tessera.NewPublicationAwaiter(ctx, readCheckpoint, 10*time.Millisecond)
198+
awaiter := NewPublicationAwaiter(ctx, readCheckpoint, 10*time.Millisecond)
199199

200200
wg := sync.WaitGroup{}
201201
for i := range 300 {
202202
index := uint64(i)
203-
future := func() (tessera.Index, error) {
203+
future := func() (Index, error) {
204204
<-time.After(15 * time.Millisecond)
205-
return tessera.Index{Index: index}, nil
205+
return Index{Index: index}, nil
206206
}
207207
wg.Add(1)
208208
go func() {
@@ -226,3 +226,29 @@ func TestAwait_multiClient(t *testing.T) {
226226
}
227227
wg.Wait()
228228
}
229+
230+
func BenchmarkAwait(b *testing.B) {
231+
cpFormat := "origin/\n%d\nhash\n\nsig"
232+
cpSize := atomic.Uint64{}
233+
readCP := func(_ context.Context) ([]byte, error) {
234+
return fmt.Appendf(nil, cpFormat, cpSize.Load()), nil
235+
}
236+
a := NewPublicationAwaiter(b.Context(), readCP, time.Millisecond)
237+
t := time.NewTicker(time.Millisecond)
238+
go func() {
239+
for {
240+
select {
241+
case <-b.Context().Done():
242+
return
243+
case <-t.C:
244+
cpSize.Add(1)
245+
}
246+
}
247+
}()
248+
f := func() (Index, error) {
249+
return Index{Index: cpSize.Load()}, nil
250+
}
251+
for b.Loop() {
252+
_, _, _ = a.Await(b.Context(), f)
253+
}
254+
}

0 commit comments

Comments
 (0)