@@ -8,11 +8,13 @@ package interactive
88
99import (
1010 "context"
11+ "sync"
1112 "time"
1213
1314 "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
1415 "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/collections/iterators"
1516 "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/events"
17+ "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/metrics"
1618 "github.com/hyperledger-labs/fabric-smart-client/platform/view/view"
1719 token2 "github.com/hyperledger-labs/fabric-token-sdk/token"
1820 "github.com/hyperledger-labs/fabric-token-sdk/token/services/logging"
@@ -41,21 +43,33 @@ type ViewManager interface {
4143 InitiateView (view view.View ) (interface {}, error )
4244}
4345
44- // CertificationClient scans the vault for tokens not yet certified and asks the certification.
46+ // CertificationClient scans the vault for tokens not yet certified and requests certification.
47+ // It batches incoming token IDs, dispatches them to a configurable worker pool, and retries
48+ // on failure. Callers must invoke Start() before using the client and Stop() to release resources.
4549type CertificationClient struct {
46- ctx context.Context
50+ ctx context.Context
51+ cancel context.CancelFunc
52+ wg sync.WaitGroup
53+
4754 channel , namespace string
4855 queryEngine QueryEngine
4956 certificationStorage CertificationStorage
5057 viewManager ViewManager
5158 certifiers []view.Identity
5259 eventOperationMap map [string ]Op
53- // waitTime is used in case of a failure. It tells how much time to wait before retrying.
54- waitTime time.Duration
55- maxAttempts int
5660
57- tokens chan * token.ID
58- batchSize int
61+ waitTime time.Duration
62+ maxAttempts int
63+ batchSize int
64+ flushInterval time.Duration
65+ workers int
66+
67+ // tokens receives individual token IDs from OnReceive and Scan.
68+ tokens chan * token.ID
69+ // batches receives assembled batches from the accumulator goroutine.
70+ batches chan []* token.ID
71+
72+ metrics * ClientMetrics
5973}
6074
6175func NewCertificationClient (
@@ -69,19 +83,31 @@ func NewCertificationClient(
6983 notifier events.Subscriber ,
7084 maxAttempts int ,
7185 waitTime time.Duration ,
86+ batchSize int ,
87+ bufferSize int ,
88+ flushInterval time.Duration ,
89+ workers int ,
90+ metricsProvider metrics.Provider ,
7291) * CertificationClient {
92+ derivedCtx , cancel := context .WithCancel (ctx )
93+
7394 cc := & CertificationClient {
74- ctx : ctx ,
95+ ctx : derivedCtx ,
96+ cancel : cancel ,
7597 channel : channel ,
7698 namespace : namespace ,
7799 queryEngine : qe ,
78100 certificationStorage : cm ,
79101 viewManager : fm ,
80102 certifiers : certifiers ,
81103 waitTime : waitTime ,
82- tokens : make (chan * token.ID , 1000 ),
83- batchSize : 10 ,
104+ tokens : make (chan * token.ID , bufferSize ),
105+ batches : make (chan []* token.ID , workers ),
106+ batchSize : batchSize ,
107+ flushInterval : flushInterval ,
108+ workers : workers ,
84109 maxAttempts : maxAttempts ,
110+ metrics : newClientMetrics (metricsProvider ),
85111 }
86112
87113 eventOperationMap := make (map [string ]Op )
@@ -107,58 +133,68 @@ func (cc *CertificationClient) RequestCertification(ctx context.Context, ids ...
107133 toBeCertified = append (toBeCertified , id )
108134 }
109135 }
136+
110137 if len (toBeCertified ) == 0 {
111138 // all tokens already certified.
112139 return nil
113140 }
114141
115142 var resultBoxed interface {}
116143 var err error
144+ labels := []string {"channel" , cc .channel , "namespace" , cc .namespace }
145+
146+ start := time .Now ()
117147 for i := range cc .maxAttempts {
118148 resultBoxed , err = cc .viewManager .InitiateView (NewCertificationRequestView (cc .channel , cc .namespace , cc .certifiers [0 ], toBeCertified ... ))
119- if err != nil {
120- logger .Errorf ("failed to request certification [%s], try again [%d] after [%s]..." , err , i , cc .waitTime )
121- time .Sleep (cc .waitTime )
122-
123- continue
149+ if err == nil {
150+ break
151+ }
152+ cc .metrics .Errors .With (labels ... ).Add (1 )
153+ logger .Errorf ("failed to request certification [%s], try again [%d] after [%s]..." , err , i , cc .waitTime )
154+ select {
155+ case <- time .After (cc .waitTime ):
156+ case <- ctx .Done ():
157+ return ctx .Err ()
124158 }
125-
126- break
127159 }
160+
128161 if err != nil {
129162 return err
130163 }
164+
165+ cc .metrics .RequestDuration .With (labels ... ).Observe (time .Since (start ).Seconds ())
166+
131167 certifications , ok := resultBoxed .(map [* token.ID ][]byte )
132168 if ! ok {
133169 return errors .Errorf ("invalid type, expected map[token.ID][]byte" )
134170 }
171+
135172 if err := cc .certificationStorage .Store (ctx , certifications ); err != nil {
136173 return err
137174 }
138175
139176 return nil
140177}
141178
179+ // Scan checks the vault for uncertified tokens and requests certification.
142180func (cc * CertificationClient ) Scan () error {
143181 logger .Debugf ("check the certification of unspent tokens from the vault..." )
144- // Check the unspent tokens
145182
146183 allTokens , err := cc .queryEngine .UnspentTokensIterator (cc .ctx )
147184 if err != nil {
148185 return errors .WithMessagef (err , "failed to get an iterator over unspent tokens" )
149186 }
150187
151188 tokenIds := iterators .Map (allTokens .UnspentTokensIterator , func (t * token.UnspentToken ) (* token.ID , error ) { return & t .Id , nil })
152- uncertifiedTokenIds := iterators .Filter (tokenIds , func (t * token.ID ) bool { return ! cc .certificationStorage .Exists (context . Background () , t ) })
189+ uncertifiedTokenIds := iterators .Filter (tokenIds , func (t * token.ID ) bool { return ! cc .certificationStorage .Exists (cc . ctx , t ) })
153190 toBeCertified , err := iterators .ReadAllPointers (uncertifiedTokenIds )
154191 if err != nil {
155192 return errors .WithMessagef (err , "failed to read tokens to be certified" )
156193 }
157194
158195 if len (toBeCertified ) != 0 {
159- // Request certification
160196 logger .Debugf ("request certification of [%v]" , toBeCertified )
161- if err := cc .RequestCertification (context . Background () , toBeCertified ... ); err != nil {
197+ if err := cc .RequestCertification (cc . ctx , toBeCertified ... ); err != nil {
162198 return errors .WithMessagef (err , "failed retrieving certification" )
163199 }
164200 logger .Debugf ("request certification of [%v] satisfied with no error" , toBeCertified )
@@ -167,85 +203,161 @@ func (cc *CertificationClient) Scan() error {
167203 return nil
168204}
169205
206+ // Start launches the accumulator goroutine and the worker pool.
207+ // It must be called before the client processes any tokens.
170208func (cc * CertificationClient ) Start () {
171- go cc .accumulatorCutter (context .Background ())
209+ for range cc .workers {
210+ cc .wg .Add (1 )
211+
212+ go func () {
213+ defer cc .wg .Done ()
214+
215+ for batch := range cc .batches {
216+ cc .processBatch (batch )
217+ }
218+ }()
219+ }
220+
221+ cc .wg .Add (1 )
222+
223+ go func () {
224+ defer cc .wg .Done ()
225+ cc .accumulatorCutter ()
226+ }()
227+ }
228+
229+ // Stop signals the client to shut down and waits for all goroutines to finish.
230+ // In-flight certification requests are completed before returning.
231+ func (cc * CertificationClient ) Stop () {
232+ cc .cancel ()
233+ cc .wg .Wait ()
172234}
173235
236+ // OnReceive handles a token-added event and enqueues the token for certification.
237+ // It is non-blocking: if the input buffer is full, the token is dropped and counted.
174238func (cc * CertificationClient ) OnReceive (event events.Event ) {
175239 t , ok := event .Message ().(tokens.TokenMessage )
176240 if ! ok {
177241 logger .Warnf ("cannot cast to TokenMessage %v" , event .Message ())
178- // drop this event
179- return
180- }
181242
182- // sanity check that we really registered for this type of event
183- _ , ok = cc .eventOperationMap [event .Topic ()]
184- if ! ok {
185- logger .Warnf ("receive an event we did not registered for %v" , event .Message ())
186- // drop this event
187243 return
188244 }
189245
190- // accumulate token
191- if len (cc .tokens ) >= cap (cc .tokens ) {
192- // skip this
193- logger .Warnf ("certification pipeline filled up, skipping id [%s:%d]" , t .TxID , t .Index )
246+ if _ , ok = cc .eventOperationMap [event .Topic ()]; ! ok {
247+ logger .Warnf ("receive an event we did not register for %v" , event .Message ())
194248
195249 return
196250 }
197- cc .tokens <- & token.ID {
251+
252+ id := & token.ID {
198253 TxId : t .TxID ,
199254 Index : t .Index ,
200255 }
256+
257+ labels := []string {"channel" , cc .channel , "namespace" , cc .namespace }
258+
259+ select {
260+ case cc .tokens <- id :
261+ cc .metrics .PendingTokens .With (labels ... ).Set (float64 (len (cc .tokens )))
262+ default :
263+ logger .Warnf ("certification pipeline filled up, dropping id [%s:%d]" , t .TxID , t .Index )
264+ cc .metrics .DroppedTokens .With (labels ... ).Add (1 )
265+ }
201266}
202267
203- func (cc * CertificationClient ) accumulatorCutter (ctx context.Context ) {
204- // TODO: introduce workers
205- timeout := time .NewTimer (5 * time .Second )
268+ // accumulatorCutter reads from the token channel and assembles batches. It sends
269+ // complete batches to the batches channel and flushes partial batches on a timer.
270+ // It closes the batches channel when it exits so that workers drain and stop.
271+ func (cc * CertificationClient ) accumulatorCutter () {
272+ defer close (cc .batches )
273+
274+ timer := time .NewTimer (cc .flushInterval )
275+ defer timer .Stop ()
276+
206277 var accumulator []* token.ID
278+
279+ flush := func () {
280+ if len (accumulator ) == 0 {
281+ return
282+ }
283+
284+ batch := accumulator
285+ accumulator = nil
286+
287+ select {
288+ case cc .batches <- batch :
289+ case <- cc .ctx .Done ():
290+ }
291+ }
292+
293+ resetTimer := func () {
294+ if ! timer .Stop () {
295+ select {
296+ case <- timer .C :
297+ default :
298+ }
299+ }
300+ timer .Reset (cc .flushInterval )
301+ }
302+
207303 for {
208304 select {
209305 case id := <- cc .tokens :
210306 logger .Debugf ("Accumulate token [%s]" , id )
211307 accumulator = append (accumulator , id )
308+
212309 if len (accumulator ) >= cc .batchSize {
213- logger .Debugf ("Limit reached, certify accumulator..." )
214- toCertify := accumulator
215- accumulator = nil
216- go cc .requestCertification (ctx , toCertify ... )
310+ logger .Debugf ("Batch limit reached, dispatching to workers..." )
311+ resetTimer ()
312+ flush ()
217313 }
218- case <- timeout .C :
219- logger .Debugf ("Timeout, certify accumulator..." )
220- toCertify := accumulator
221- accumulator = nil
222- go cc .requestCertification (ctx , toCertify ... )
314+
315+ case <- timer .C :
316+ logger .Debugf ("Flush interval reached, dispatching partial batch..." )
317+ flush ()
318+ timer .Reset (cc .flushInterval )
319+
223320 case <- cc .ctx .Done ():
224- // time to close
321+ // Flush any remaining tokens before exiting.
322+ flush ()
323+
225324 return
226325 }
227326 }
228327}
229328
230- func (cc * CertificationClient ) requestCertification (ctx context.Context , tokens ... * token.ID ) {
231- if len (tokens ) == 0 {
232- // no tokens passed, check the vault
233- logger .Debugf ("request certification of 0 tokens, check the vault..." )
329+ // processBatch certifies a batch of tokens. On failure it pushes uncertified tokens
330+ // back to the input channel using a non-blocking send to avoid deadlocks.
331+ func (cc * CertificationClient ) processBatch (batch []* token.ID ) {
332+ if len (batch ) == 0 {
333+ // empty batch: scan the vault for uncertified tokens
334+ logger .Debugf ("processBatch: empty batch, scanning vault..." )
234335 if err := cc .Scan (); err != nil {
235- logger .Errorf ("failed to scan the vault for token to be certified [%s]" , err )
336+ logger .Errorf ("failed to scan the vault for tokens to be certified [%s]" , err )
236337 }
237338
238339 return
239340 }
240- logger .Debugf ("request certification of [%v]" , tokens )
241- if err := cc .RequestCertification (ctx , tokens ... ); err != nil {
242- // push back the ids
243- logger .Warnf ("failed retrieving certification [%s], push back token ids [%s]" , err , tokens )
244- for _ , id := range tokens {
245- cc .tokens <- id
341+
342+ logger .Debugf ("request certification of [%v]" , batch )
343+
344+ if err := cc .RequestCertification (cc .ctx , batch ... ); err != nil {
345+ // Push uncertified tokens back with a non-blocking send to avoid deadlock.
346+ labels := []string {"channel" , cc .channel , "namespace" , cc .namespace }
347+
348+ logger .Warnf ("failed retrieving certification [%s], attempting to re-queue tokens" , err )
349+
350+ for _ , id := range batch {
351+ select {
352+ case cc .tokens <- id :
353+ default :
354+ logger .Warnf ("certification buffer full after failure, dropping token [%s]" , id )
355+ cc .metrics .DroppedTokens .With (labels ... ).Add (1 )
356+ }
246357 }
247358
248359 return
249360 }
250- logger .Debugf ("request certification of [%v] satisfied with no error" , tokens )
361+
362+ logger .Debugf ("certification of [%v] succeeded" , batch )
251363}
0 commit comments