@@ -17,13 +17,10 @@ package tessera
1717import (
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.
3431func 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.
5653type 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.
8571func (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.
10297func (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- }
0 commit comments