Skip to content

Commit 3dfa081

Browse files
committed
Fix review comments
Signed-off-by: AkramBitar <akram@il.ibm.com>
1 parent 8870429 commit 3dfa081

3 files changed

Lines changed: 50 additions & 6 deletions

File tree

docs/services/network-fabric.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,19 @@ replay finality notifications for every historical transaction, while the caller
308308
no way to tell a transient RPC failure from a genuinely fresh chain. Block 0 is used only
309309
when no ledger is configured at all.
310310

311+
The returned error is classifiable with `errors.Is`, so a caller does not have to match on
312+
its message:
313+
314+
| Sentinel | Meaning | Reaction |
315+
| --- | --- | --- |
316+
| `finality.ErrLedgerHeightUnavailable` | the height could not be read within the attempt budget, so no scan started | block-delivery finality is degraded; fall back to query-based finality and retry later |
317+
| `finality.ErrNoLedgerInfo` | the ledger returned neither info nor an error — a driver contract violation | will not self-heal; report as a bug |
318+
| `context.Canceled` / `context.DeadlineExceeded` | the context ended the retries | shutdown, not a failure |
319+
320+
`ErrNoLedgerInfo` is always accompanied by `ErrLedgerHeightUnavailable`, and the underlying
321+
ledger error is preserved in every case. An error matching none of these comes from the
322+
block scan itself rather than from resolving the starting block.
323+
311324
### Notification Mode
312325

313326
Uses asynchronous event notifications from the FSC layer:

token/services/network/fabric/finality/delivery.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ const (
2525
defaultLedgerInfoRetryDelay = 500 * time.Millisecond
2626
)
2727

28+
var (
29+
// ErrNoLedgerInfo reports that the ledger returned neither info nor an error,
30+
// so no height could be read. It signals a driver contract violation rather
31+
// than a transient failure: retrying it is not expected to help.
32+
ErrNoLedgerInfo = errors.New("ledger returned no info")
33+
// ErrLedgerHeightUnavailable reports that the current ledger height could not
34+
// be read within the attempt budget, so the block scan was refused instead of
35+
// restarted from genesis. It distinguishes a failure to decide where to scan
36+
// from a failure of the scan itself; the last observed cause remains
37+
// inspectable with errors.Is.
38+
ErrLedgerHeightUnavailable = errors.New("ledger height unavailable")
39+
)
40+
2841
// blockFromScanner is the subset of fabric.Delivery used by Delivery: the block
2942
// scan, parameterized by the block to start from.
3043
type blockFromScanner interface {
@@ -62,6 +75,12 @@ type Delivery struct {
6275
//
6376
// A nil Ledger is the one case that still starts at block 0 — there is no height
6477
// to read, so the whole chain is the intended range.
78+
//
79+
// The returned error is classifiable without inspecting its message: it matches
80+
// ErrLedgerHeightUnavailable when the height could not be read (and additionally
81+
// ErrNoLedgerInfo when the ledger returned neither info nor an error), and
82+
// context.Canceled or context.DeadlineExceeded when the context ended the
83+
// retries. Any other error comes from the scan itself.
6584
func (d *Delivery) ScanBlock(background context.Context, callback fabric.BlockCallback) error {
6685
startingBlock := uint64(0)
6786
if d.Ledger != nil {
@@ -78,6 +97,12 @@ func (d *Delivery) ScanBlock(background context.Context, callback fabric.BlockCa
7897
// ledgerHeight returns the current ledger height, retrying transient failures
7998
// with an exponentially growing delay. It gives up as soon as the context is
8099
// cancelled, and reports the last observed failure once the attempts run out.
100+
//
101+
// Failures are joined with a sentinel rather than described only in the message,
102+
// so callers classify them with errors.Is: ErrLedgerHeightUnavailable once the
103+
// budget is spent, ErrNoLedgerInfo for a driver returning (nil, nil), and the
104+
// context error for a cancelled wait. The underlying ledger error is preserved
105+
// in every case.
81106
func (d *Delivery) ledgerHeight(ctx context.Context) (uint64, error) {
82107
attempts := d.LedgerInfoAttempts
83108
if attempts <= 0 {
@@ -97,7 +122,7 @@ func (d *Delivery) ledgerHeight(ctx context.Context) (uint64, error) {
97122
case info == nil:
98123
// A driver that reports neither info nor error would otherwise nil
99124
// deref below; treat it as a failure to read the height.
100-
lastErr = errors.New("ledger returned no info")
125+
lastErr = ErrNoLedgerInfo
101126
default:
102127
return info.Height, nil
103128
}
@@ -110,11 +135,11 @@ func (d *Delivery) ledgerHeight(ctx context.Context) (uint64, error) {
110135

111136
select {
112137
case <-ctx.Done():
113-
return 0, errors.Wrapf(ctx.Err(), "cancelled while getting ledger info, last error [%v]", lastErr)
138+
return 0, errors.Wrap(errors.Join(ctx.Err(), lastErr), "cancelled while getting ledger info")
114139
case <-time.After(delay):
115140
}
116141
delay *= 2
117142
}
118143

119-
return 0, errors.Wrapf(lastErr, "failed to get ledger info after %d attempt(s), refusing to rescan from genesis", attempts)
144+
return 0, errors.Wrapf(errors.Join(ErrLedgerHeightUnavailable, lastErr), "failed to get ledger info after %d attempt(s), refusing to rescan from genesis", attempts)
120145
}

token/services/network/fabric/finality/delivery_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ func TestScanBlock_PropagatesLedgerInfoErrorInsteadOfRescanningFromGenesis(t *te
109109

110110
require.Error(t, err, "GetLedgerInfo failure must be propagated, not swallowed after a log line")
111111
require.ErrorIs(t, err, transient, "the underlying ledger error must stay inspectable")
112-
assert.Contains(t, err.Error(), "refusing to rescan from genesis")
112+
require.ErrorIs(t, err, finality.ErrLedgerHeightUnavailable, "the caller must classify this without matching on the message")
113+
require.NotErrorIs(t, err, finality.ErrNoLedgerInfo, "an unreachable peer is not a driver returning no info")
113114
assert.False(t, delivery.called, "no scan may start from genesis when the ledger height is unknown")
114115
}
115116

@@ -154,14 +155,17 @@ func TestScanBlock_NilLedgerInfoIsReportedNotDereferenced(t *testing.T) {
154155
err := newTestDelivery(ledger, delivery).ScanBlock(context.Background(), nil)
155156

156157
require.Error(t, err)
157-
assert.Contains(t, err.Error(), "ledger returned no info")
158+
require.ErrorIs(t, err, finality.ErrNoLedgerInfo, "the (nil, nil) contract violation must be identifiable, not just described")
159+
require.ErrorIs(t, err, finality.ErrLedgerHeightUnavailable, "it is also a failure to read the height")
160+
require.NotErrorIs(t, err, context.Canceled, "a nil-info driver is not a cancellation")
158161
assert.False(t, delivery.called)
159162
}
160163

161164
// TestScanBlock_CancelledContextStopsRetrying verifies shutdown does not have to
162165
// wait out the retry budget: a cancelled context ends the loop with its own error.
163166
func TestScanBlock_CancelledContextStopsRetrying(t *testing.T) {
164-
ledger := &fakeHeightLedger{results: []heightLedgerResult{{err: errors.New("peer connection reset")}}}
167+
transient := errors.New("peer connection reset")
168+
ledger := &fakeHeightLedger{results: []heightLedgerResult{{err: transient}}}
165169
delivery := &recordingBlockDelivery{}
166170

167171
d := newTestDelivery(ledger, delivery)
@@ -177,6 +181,7 @@ func TestScanBlock_CancelledContextStopsRetrying(t *testing.T) {
177181
select {
178182
case err := <-done:
179183
require.ErrorIs(t, err, context.Canceled)
184+
require.ErrorIs(t, err, transient, "the failure that was being retried must stay inspectable alongside the cancellation")
180185
assert.Equal(t, int32(1), ledger.calls.Load(), "must not keep retrying after cancellation")
181186
assert.False(t, delivery.called)
182187
case <-time.After(10 * time.Second):
@@ -206,5 +211,6 @@ func TestScanBlock_PropagatesScanError(t *testing.T) {
206211
err := newTestDelivery(ledger, delivery).ScanBlock(context.Background(), nil)
207212

208213
require.ErrorIs(t, err, scanErr)
214+
require.NotErrorIs(t, err, finality.ErrLedgerHeightUnavailable, "a scan failure must not be mistaken for a failure to read the height")
209215
assert.Equal(t, uint64(3), delivery.startingBlock)
210216
}

0 commit comments

Comments
 (0)