Skip to content

Commit 26270d4

Browse files
Merge pull request #8134 from onflow/andron/7655-registers-endpoints-error-handling
2 parents 4447798 + 723235e commit 26270d4

File tree

6 files changed

+229
-70
lines changed

6 files changed

+229
-70
lines changed

engine/access/state_stream/backend/backend.go

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ import (
77
"time"
88

99
"github.com/rs/zerolog"
10-
"google.golang.org/grpc/codes"
11-
"google.golang.org/grpc/status"
1210

11+
"github.com/onflow/flow-go/access"
1312
"github.com/onflow/flow-go/engine/access/index"
1413
"github.com/onflow/flow-go/engine/access/rpc/backend/common"
1514
"github.com/onflow/flow-go/engine/access/state_stream"
@@ -188,61 +187,76 @@ func (b *StateStreamBackend) getExecutionData(ctx context.Context, height uint64
188187
}
189188

190189
// GetRegisterValues returns the register values for the given register IDs at the given block height.
190+
//
191+
// CAUTION: this layer SIMPLIFIES the ERROR HANDLING convention
192+
// - All errors returned are guaranteed to be benign. The node can continue normal operations after such errors.
193+
// - To prevent delivering incorrect results to clients in case of an error, all other return values should be discarded.
194+
//
195+
// Expected sentinel errors providing details to clients about failed requests:
196+
// - [access.InvalidRequestError]: If the request had invalid arguments.
197+
// - [access.DataNotFoundError]: When data required to process the request is not available.
198+
// - [access.OutOfRangeError]: If the data for the requested height is outside the node's available range.
199+
// - [access.PreconditionFailedError]: When the register's database isn't initialized yet.
191200
func (b *StateStreamBackend) GetRegisterValues(
201+
ctx context.Context,
192202
ids flow.RegisterIDs,
193203
height uint64,
194204
criteria optimistic_sync.Criteria,
195205
) ([]flow.RegisterValue, *accessmodel.ExecutorMetadata, error) {
196206
if len(ids) > b.registerRequestLimit {
197-
return nil, nil, status.Errorf(codes.InvalidArgument, "number of register IDs exceeds limit of %d", b.registerRequestLimit)
207+
return nil, nil, access.NewInvalidRequestError(
208+
fmt.Errorf("number of register IDs exceeds limit of %d", b.registerRequestLimit))
198209
}
199210

200211
header, err := b.headers.ByHeight(height)
201212
if err != nil {
202-
if errors.Is(err, storage.ErrNotFound) {
203-
return nil, nil, status.Errorf(codes.NotFound, "no finalized block is known at the given height %d", height)
204-
}
205-
return nil, nil, err
213+
err = access.RequireErrorIs(ctx, err, storage.ErrNotFound)
214+
err = fmt.Errorf("failed to find header by height %d: %w", height, err)
215+
return nil, nil, access.NewDataNotFoundError("header", err)
206216
}
207217

208218
execResultInfo, err := b.executionResultProvider.ExecutionResultInfo(header.ID(), criteria)
209219
if err != nil {
210-
if errors.Is(err, storage.ErrNotFound) || common.IsInsufficientExecutionReceipts(err) {
211-
return nil, nil, status.Errorf(codes.NotFound, "failed to get execution result info for block")
220+
err = fmt.Errorf("failed to get execution result info for block: %w", err)
221+
switch {
222+
case errors.Is(err, storage.ErrNotFound):
223+
return nil, nil, access.NewDataNotFoundError("execution data", err)
224+
case common.IsInsufficientExecutionReceipts(err):
225+
return nil, nil, access.NewDataNotFoundError("execution data", err)
226+
default:
227+
return nil, nil, access.RequireNoError(ctx, err)
212228
}
213-
return nil, nil, err
214229
}
215230

216231
executionResultID := execResultInfo.ExecutionResultID
217232
snapshot, err := b.executionStateCache.Snapshot(executionResultID)
218233
if err != nil {
219-
if errors.Is(err, storage.ErrNotFound) {
220-
return nil, nil, status.Errorf(codes.NotFound, "register values for block %d not found", height)
221-
}
222-
return nil, nil, err
234+
err = access.RequireErrorIs(ctx, err, storage.ErrNotFound)
235+
err = fmt.Errorf("failed to find snapshot by execution result ID %s: %w", executionResultID.String(), err)
236+
return nil, nil, access.NewDataNotFoundError("snapshot", err)
223237
}
224238

225239
registers, err := snapshot.Registers()
226240
if err != nil {
227-
if errors.Is(err, indexer.ErrIndexNotInitialized) {
228-
return nil, nil, status.Errorf(codes.FailedPrecondition, "failed to get lowest indexed height: %v", err)
229-
}
230-
231-
return nil, nil, err
241+
err = access.RequireErrorIs(ctx, err, indexer.ErrIndexNotInitialized)
242+
err = fmt.Errorf("failed to get registers storage from snapshot: %w", err)
243+
return nil, nil, access.NewPreconditionFailedError(err)
232244
}
233245

234246
result := make([]flow.RegisterValue, len(ids))
235247
for i, regID := range ids {
236248
val, err := registers.Get(regID, height)
237249
if err != nil {
238-
if errors.Is(err, storage.ErrHeightNotIndexed) {
239-
return nil, nil, status.Errorf(codes.OutOfRange, "register values for block %d is not available", height)
240-
}
241-
if errors.Is(err, storage.ErrNotFound) {
242-
return nil, nil, status.Errorf(codes.NotFound, "register values for block %d not found", height)
250+
err = fmt.Errorf("failed to get register by the register ID at a given block height: %w", err)
251+
switch {
252+
case errors.Is(err, storage.ErrNotFound):
253+
return nil, nil, access.NewDataNotFoundError("registers", err)
254+
case errors.Is(err, storage.ErrHeightNotIndexed):
255+
return nil, nil, access.NewOutOfRangeError(err)
256+
default:
257+
return nil, nil, access.RequireNoError(ctx, err)
243258
}
244259

245-
return nil, nil, err
246260
}
247261
result[i] = val
248262
}

engine/access/state_stream/backend/backend_executiondata_test.go

Lines changed: 149 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -406,13 +406,13 @@ func (s *BackendExecutionDataSuite) TestGetExecutionDataByBlockID() {
406406
})
407407

408408
s.Run("execution result info returns unexpected error", func() {
409-
expectedErr := fmt.Errorf("failed to get execution result info for block: %w", storage.ErrDataMismatch)
409+
exception := fmt.Errorf("failed to get execution result info for block: %w", storage.ErrDataMismatch)
410410
s.executionResultProvider.
411411
On("ExecutionResultInfo", block.ID(), mock.Anything).
412412
Return(nil, storage.ErrDataMismatch).
413413
Once()
414414

415-
ctxSignaler := irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, expectedErr)
415+
ctxSignaler := irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, exception)
416416
ctxIrr := irrecoverable.WithSignalerContext(ctx, ctxSignaler)
417417

418418
execDataRes, metadata, err := s.backend.GetExecutionDataByBlockID(ctxIrr, block.ID(), s.criteria)
@@ -451,13 +451,13 @@ func (s *BackendExecutionDataSuite) TestGetExecutionDataByBlockID() {
451451
}, nil).
452452
Once()
453453

454-
expectedError := fmt.Errorf("unexpected error")
454+
exception := fmt.Errorf("unexpected error")
455455
s.executionStateCache.
456456
On("Snapshot", result.ID()).
457-
Return(nil, expectedError).
457+
Return(nil, exception).
458458
Once()
459459

460-
ctxSignaler := irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, expectedError)
460+
ctxSignaler := irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, exception)
461461
ctxIrr := irrecoverable.WithSignalerContext(ctx, ctxSignaler)
462462

463463
execDataRes, metadata, err := s.backend.GetExecutionDataByBlockID(ctxIrr, block.ID(), s.criteria)
@@ -522,8 +522,8 @@ func (s *BackendExecutionDataSuite) TestGetExecutionDataByBlockID() {
522522
Return(nil, storage.ErrDataMismatch).
523523
Once()
524524

525-
expectedError := fmt.Errorf("unexpected error getting execution data: %w", storage.ErrDataMismatch)
526-
ctxSignaler := irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, expectedError)
525+
exception := fmt.Errorf("unexpected error getting execution data: %w", storage.ErrDataMismatch)
526+
ctxSignaler := irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, exception)
527527
ctxIrr := irrecoverable.WithSignalerContext(ctx, ctxSignaler)
528528

529529
execDataRes, metadata, err := s.backend.GetExecutionDataByBlockID(ctxIrr, block.ID(), s.criteria)
@@ -897,6 +897,9 @@ func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataHandlesErrors() {
897897
// TestGetRegisterValues tests that GetRegisterValues correctly returns register data
898898
// in normal conditions and propagates appropriate errors for all failure scenarios.
899899
func (s *BackendExecutionDataSuite) TestGetRegisterValues() {
900+
ctx, cancel := context.WithCancel(context.Background())
901+
defer cancel()
902+
900903
block := s.blocks[0]
901904
seal := s.sealMap[block.ID()]
902905
result := s.resultMap[seal.ResultID]
@@ -928,41 +931,60 @@ func (s *BackendExecutionDataSuite) TestGetRegisterValues() {
928931
expectedRegister := flow.RegisterValue("value0")
929932
s.registers.On("Get", s.registerID, block.Height).Return(expectedRegister, nil).Once()
930933

931-
res, resMetadata, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
934+
res, resMetadata, err := s.backend.GetRegisterValues(ctx, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
932935

933936
require.Equal(s.T(), []flow.RegisterValue{expectedRegister}, res)
934937
require.Equal(s.T(), metadata, resMetadata)
935938
require.NoError(s.T(), err)
936939
})
937940

938941
s.Run("returns error if too many registers are requested", func() {
939-
res, metadata, err := s.backend.GetRegisterValues(make(flow.RegisterIDs, s.backend.registerRequestLimit+1), block.Height, s.criteria)
942+
res, metadata, err := s.backend.GetRegisterValues(ctx, make(flow.RegisterIDs, s.backend.registerRequestLimit+1), block.Height, s.criteria)
940943

941944
require.Nil(s.T(), res)
942945
require.Nil(s.T(), metadata)
943-
require.Equal(s.T(), codes.InvalidArgument, status.Code(err))
946+
require.Error(s.T(), err)
947+
require.True(s.T(), access.IsInvalidRequestError(err))
944948
})
945949

946950
s.Run("returns error if failed to get execution result info for block - insufficient receipts", func() {
947951
s.executionResultProvider.
948952
On("ExecutionResultInfo", block.ToHeader().ID(), mock.Anything).
949953
Return(nil, common.NewInsufficientExecutionReceipts(block.ID(), 0)).Once()
950954

951-
res, metadata, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
955+
res, metadata, err := s.backend.GetRegisterValues(ctx, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
952956
require.Nil(s.T(), res)
953957
require.Nil(s.T(), metadata)
954-
require.Equal(s.T(), codes.NotFound, status.Code(err))
958+
require.Error(s.T(), err)
959+
require.True(s.T(), access.IsDataNotFoundError(err))
955960
})
956961

957962
s.Run("returns error if failed to get execution result info for block - not found", func() {
958963
s.executionResultProvider.
959964
On("ExecutionResultInfo", block.ToHeader().ID(), mock.Anything).
960965
Return(nil, storage.ErrNotFound).Once()
961966

962-
res, metadata, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
967+
res, metadata, err := s.backend.GetRegisterValues(ctx, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
968+
require.Nil(s.T(), res)
969+
require.Nil(s.T(), metadata)
970+
require.Error(s.T(), err)
971+
require.True(s.T(), access.IsDataNotFoundError(err))
972+
})
973+
974+
s.Run("execution result info returns unexpected error", func() {
975+
exception := fmt.Errorf("failed to get execution result info for block: %w", storage.ErrDataMismatch)
976+
977+
s.executionResultProvider.
978+
On("ExecutionResultInfo", block.ToHeader().ID(), mock.Anything).
979+
Return(nil, storage.ErrDataMismatch).Once()
980+
981+
ctxSignaler := irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, exception)
982+
ctxIrr := irrecoverable.WithSignalerContext(ctx, ctxSignaler)
983+
984+
res, metadata, err := s.backend.GetRegisterValues(ctxIrr, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
963985
require.Nil(s.T(), res)
964986
require.Nil(s.T(), metadata)
965-
require.Equal(s.T(), codes.NotFound, status.Code(err))
987+
require.Error(s.T(), err)
966988
})
967989

968990
s.Run("returns error when snapshot is not found", func() {
@@ -978,10 +1000,34 @@ func (s *BackendExecutionDataSuite) TestGetRegisterValues() {
9781000
Return(nil, storage.ErrNotFound).
9791001
Once()
9801002

981-
res, metadata, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
1003+
res, metadata, err := s.backend.GetRegisterValues(ctx, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
9821004
require.Nil(s.T(), res)
9831005
require.Nil(s.T(), metadata)
984-
require.Equal(s.T(), codes.NotFound, status.Code(err))
1006+
require.Error(s.T(), err)
1007+
require.True(s.T(), access.IsDataNotFoundError(err))
1008+
})
1009+
1010+
s.Run("snapshot returns unexpected error", func() {
1011+
s.executionResultProvider.
1012+
On("ExecutionResultInfo", block.ToHeader().ID(), mock.Anything).
1013+
Return(&optimistic_sync.ExecutionResultInfo{
1014+
ExecutionResultID: result.ID(),
1015+
ExecutionNodes: executionNodes.ToSkeleton(),
1016+
}, nil).Once()
1017+
1018+
exception := fmt.Errorf("unexpected error")
1019+
s.executionStateCache.
1020+
On("Snapshot", result.ID()).
1021+
Return(nil, exception).
1022+
Once()
1023+
1024+
ctxSignaler := irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, exception)
1025+
ctxIrr := irrecoverable.WithSignalerContext(ctx, ctxSignaler)
1026+
1027+
execDataRes, metadata, err := s.backend.GetExecutionDataByBlockID(ctxIrr, block.ID(), s.criteria)
1028+
assert.Nil(s.T(), execDataRes)
1029+
assert.Nil(s.T(), metadata)
1030+
assert.Error(s.T(), err)
9851031
})
9861032

9871033
s.Run("returns error if the storage is still bootstrapping.", func() {
@@ -999,10 +1045,36 @@ func (s *BackendExecutionDataSuite) TestGetRegisterValues() {
9991045

10001046
s.executionDataSnapshot.On("Registers").Return(nil, indexer.ErrIndexNotInitialized).Once()
10011047

1002-
res, metadata, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
1048+
res, metadata, err := s.backend.GetRegisterValues(ctx, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
1049+
require.Nil(s.T(), res)
1050+
require.Nil(s.T(), metadata)
1051+
require.Error(s.T(), err)
1052+
require.True(s.T(), access.IsPreconditionFailedError(err))
1053+
})
1054+
1055+
s.Run("registers returns unexpected error", func() {
1056+
s.executionResultProvider.
1057+
On("ExecutionResultInfo", block.ToHeader().ID(), mock.Anything).
1058+
Return(&optimistic_sync.ExecutionResultInfo{
1059+
ExecutionResultID: result.ID(),
1060+
ExecutionNodes: executionNodes.ToSkeleton(),
1061+
}, nil).Once()
1062+
1063+
s.executionStateCache.
1064+
On("Snapshot", result.ID()).
1065+
Return(s.executionDataSnapshot, nil).
1066+
Once()
1067+
1068+
exception := fmt.Errorf("unexpected error")
1069+
s.executionDataSnapshot.On("Registers").Return(nil, exception).Once()
1070+
1071+
ctxSignaler := irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, exception)
1072+
ctxIrr := irrecoverable.WithSignalerContext(ctx, ctxSignaler)
1073+
1074+
res, metadata, err := s.backend.GetRegisterValues(ctxIrr, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
10031075
require.Nil(s.T(), res)
10041076
require.Nil(s.T(), metadata)
1005-
require.Equal(s.T(), codes.FailedPrecondition, status.Code(err))
1077+
require.Error(s.T(), err)
10061078
})
10071079

10081080
s.Run("returns error if the requested height is outside the range of indexed blocks", func() {
@@ -1022,10 +1094,34 @@ func (s *BackendExecutionDataSuite) TestGetRegisterValues() {
10221094

10231095
s.registers.On("Get", s.registerID, block.Height).Return(nil, storage.ErrHeightNotIndexed).Once()
10241096

1025-
res, metadata, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
1097+
res, metadata, err := s.backend.GetRegisterValues(ctx, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
1098+
require.Nil(s.T(), res)
1099+
require.Nil(s.T(), metadata)
1100+
require.Error(s.T(), err)
1101+
require.ErrorIs(s.T(), err, storage.ErrHeightNotIndexed)
1102+
})
1103+
1104+
s.Run("returns error if block or registerSnapshot value at height was not found", func() {
1105+
s.executionResultProvider.
1106+
On("ExecutionResultInfo", block.ToHeader().ID(), mock.Anything).
1107+
Return(&optimistic_sync.ExecutionResultInfo{
1108+
ExecutionResultID: result.ID(),
1109+
ExecutionNodes: executionNodes.ToSkeleton(),
1110+
}, nil).Once()
1111+
1112+
s.executionStateCache.
1113+
On("Snapshot", result.ID()).
1114+
Return(s.executionDataSnapshot, nil).
1115+
Once()
1116+
1117+
s.executionDataSnapshot.On("Registers").Return(s.registers, nil).Once()
1118+
s.registers.On("Get", s.registerID, block.Height).Return(nil, storage.ErrNotFound).Once()
1119+
1120+
res, metadata, err := s.backend.GetRegisterValues(ctx, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
10261121
require.Nil(s.T(), res)
10271122
require.Nil(s.T(), metadata)
1028-
require.Equal(s.T(), codes.OutOfRange, status.Code(err))
1123+
require.Error(s.T(), err)
1124+
require.True(s.T(), access.IsDataNotFoundError(err))
10291125
})
10301126

10311127
s.Run("returns error if block or registerSnapshot value at height was not found", func() {
@@ -1044,19 +1140,48 @@ func (s *BackendExecutionDataSuite) TestGetRegisterValues() {
10441140
s.executionDataSnapshot.On("Registers").Return(s.registers, nil).Once()
10451141
s.registers.On("Get", s.registerID, block.Height).Return(nil, storage.ErrNotFound).Once()
10461142

1047-
res, metadata, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
1143+
res, metadata, err := s.backend.GetRegisterValues(ctx, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
1144+
require.Nil(s.T(), res)
1145+
require.Nil(s.T(), metadata)
1146+
require.Error(s.T(), err)
1147+
require.True(s.T(), access.IsDataNotFoundError(err))
1148+
})
1149+
1150+
s.Run("registers Get returns unexpected error", func() {
1151+
s.executionResultProvider.
1152+
On("ExecutionResultInfo", block.ToHeader().ID(), mock.Anything).
1153+
Return(&optimistic_sync.ExecutionResultInfo{
1154+
ExecutionResultID: result.ID(),
1155+
ExecutionNodes: executionNodes.ToSkeleton(),
1156+
}, nil).Once()
1157+
1158+
s.executionStateCache.
1159+
On("Snapshot", result.ID()).
1160+
Return(s.executionDataSnapshot, nil).
1161+
Once()
1162+
1163+
s.executionDataSnapshot.On("Registers").Return(s.registers, nil).Once()
1164+
1165+
exception := fmt.Errorf("failed to get register by the register ID at a given block height: %w", storage.ErrDataMismatch)
1166+
ctxSignaler := irrecoverable.NewMockSignalerContextExpectError(s.T(), ctx, exception)
1167+
ctxIrr := irrecoverable.WithSignalerContext(ctx, ctxSignaler)
1168+
1169+
s.registers.On("Get", s.registerID, block.Height).Return(nil, storage.ErrDataMismatch).Once()
1170+
1171+
res, metadata, err := s.backend.GetRegisterValues(ctxIrr, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
10481172
require.Nil(s.T(), res)
10491173
require.Nil(s.T(), metadata)
1050-
require.Equal(s.T(), codes.NotFound, status.Code(err))
1174+
require.Error(s.T(), err)
10511175
})
10521176

10531177
s.Run("returns error if no finalized block is known at the given height", func() {
10541178
s.headers.On("ByHeight", block.Height).Unset()
10551179
s.headers.On("ByHeight", block.Height).Return(nil, storage.ErrNotFound)
10561180

1057-
res, metadata, err := s.backend.GetRegisterValues(flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
1181+
res, metadata, err := s.backend.GetRegisterValues(ctx, flow.RegisterIDs{s.registerID}, block.Height, s.criteria)
10581182
require.Nil(s.T(), res)
10591183
require.Nil(s.T(), metadata)
1060-
require.Equal(s.T(), codes.NotFound, status.Code(err))
1184+
require.Error(s.T(), err)
1185+
require.True(s.T(), access.IsDataNotFoundError(err))
10611186
})
10621187
}

0 commit comments

Comments
 (0)