Skip to content

Commit e2f0f86

Browse files
authored
fix(certifier): harden interactive client, add config, workers, and metrics (#1471)
Signed-off-by: atharrva01 <atharvaborade568@gmail.com>
1 parent 887ef85 commit e2f0f86

7 files changed

Lines changed: 802 additions & 92 deletions

File tree

token/services/certifier/interactive/client.go

Lines changed: 172 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ package interactive
88

99
import (
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.
4549
type 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

6175
func 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.
142180
func (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.
170208
func (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.
174238
func (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

Comments
 (0)