Skip to content

Commit 7f9364d

Browse files
Merge pull request #1647 from hyperledger/dup-batch-handle
Update handling of duplicate batches to avoid editing existing messages
2 parents 490539e + d934bcc commit 7f9364d

File tree

4 files changed

+63
-22
lines changed

4 files changed

+63
-22
lines changed

internal/events/batch_pin_complete_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -660,8 +660,9 @@ func TestPersistBatchGoodDataMessageFail(t *testing.T) {
660660

661661
em.mdi.On("InsertOrGetBatch", mock.Anything, mock.Anything).Return(nil, nil)
662662
em.mdi.On("InsertDataArray", mock.Anything, mock.Anything).Return(nil)
663-
em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimzation miss"))
664-
em.mdi.On("UpsertMessage", mock.Anything, mock.Anything, database.UpsertOptimizationExisting, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("pop"))
663+
em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimzation miss")).Once()
664+
em.mdi.On("GetMessageIDs", mock.Anything, "ns1", mock.Anything).Return([]*core.IDAndSequence{}, nil)
665+
em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("pop")).Once()
665666

666667
em.mim.On("GetLocalNode", mock.Anything).Return(testNode, nil)
667668

internal/events/dx_callbacks_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ func TestMessageReceiveMessagePersistMessageFail(t *testing.T) {
609609
em.mdi.On("InsertOrGetBatch", em.ctx, mock.Anything).Return(nil, nil)
610610
em.mdi.On("InsertDataArray", em.ctx, mock.Anything).Return(nil)
611611
em.mdi.On("InsertMessages", em.ctx, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimization fail"))
612-
em.mdi.On("UpsertMessage", em.ctx, mock.Anything, database.UpsertOptimizationExisting, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("pop"))
612+
em.mdi.On("GetMessageIDs", mock.Anything, "ns1", mock.Anything).Return(nil, fmt.Errorf("pop"))
613613

614614
// no ack as we are simulating termination mid retry
615615
mde := newMessageReceivedNoAck("peer1", tw)

internal/events/persist_batch.go

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ package events
1818

1919
import (
2020
"context"
21+
"database/sql/driver"
2122

23+
"github.com/hyperledger/firefly-common/pkg/ffapi"
2224
"github.com/hyperledger/firefly-common/pkg/fftypes"
2325
"github.com/hyperledger/firefly-common/pkg/log"
2426
"github.com/hyperledger/firefly/pkg/core"
@@ -298,22 +300,56 @@ func (em *eventManager) persistBatchContent(ctx context.Context, batch *core.Bat
298300
})
299301
if err != nil {
300302
log.L(ctx).Debugf("Batch message insert optimization failed for batch '%s': %s", batch.ID, err)
301-
// Fall back to individual upserts
302-
for i, msg := range batch.Payload.Messages {
303-
postHookUpdateMessageCache := func() {
304-
mm := matchedMsgs[i]
305-
em.data.UpdateMessageCache(mm.message, mm.data)
306-
}
307-
if err = em.database.UpsertMessage(ctx, msg, database.UpsertOptimizationExisting, postHookUpdateMessageCache); err != nil {
308-
if err == database.HashMismatch {
309-
log.L(ctx).Errorf("Invalid message entry %d in batch'%s'. Hash mismatch with existing record with same UUID '%s' Hash=%s", i, batch.ID, msg.Header.ID, msg.Hash)
310-
return false, nil // This is not retryable. skip this data entry
303+
304+
// Messages are immutable in their contents, and it's entirely possible we're being sent
305+
// messages we've already been sent in a previous batch, and subsequently modified th
306+
// state of (they've been confirmed etc.).
307+
// So we find a list of those that aren't in the DB and so and insert just those.
308+
var foundIDs []*core.IDAndSequence
309+
foundIDs, err = em.database.GetMessageIDs(ctx, batch.Namespace, messageIDFilter(ctx, batch.Payload.Messages))
310+
if err == nil {
311+
remainingInserts := make([]*core.Message, 0, len(batch.Payload.Messages))
312+
for _, m := range batch.Payload.Messages {
313+
isFound := false
314+
for _, foundID := range foundIDs {
315+
if foundID.ID.Equals(m.Header.ID) {
316+
isFound = true
317+
log.L(ctx).Warnf("Message %s in batch '%s' is a duplicate", m.Header.ID, batch.ID)
318+
break
319+
}
320+
}
321+
if !isFound {
322+
remainingInserts = append(remainingInserts, m)
311323
}
312-
log.L(ctx).Errorf("Failed to insert message entry %d in batch '%s': %s", i, batch.ID, err)
313-
return false, err // a persistence failure here is considered retryable (so returned)
324+
}
325+
if len(remainingInserts) > 0 {
326+
// Only the remaining ones get updates
327+
err = em.database.InsertMessages(ctx, batch.Payload.Messages, func() {
328+
for _, mm := range matchedMsgs {
329+
for _, m := range remainingInserts {
330+
if mm.message.Header.ID.Equals(m.Header.ID) {
331+
em.data.UpdateMessageCache(mm.message, mm.data)
332+
}
333+
}
334+
}
335+
})
314336
}
315337
}
338+
// If we have an error at this point, we cannot insert (must not be a duplicate)
339+
if err != nil {
340+
log.L(ctx).Errorf("Failed to insert messages: %s", err)
341+
return false, err // a persistence failure here is considered retryable (so returned)
342+
}
316343
}
317344

318345
return true, nil
319346
}
347+
348+
func messageIDFilter(ctx context.Context, msgs []*core.Message) ffapi.Filter {
349+
fb := database.MessageQueryFactory.NewFilter(ctx)
350+
ids := make([]driver.Value, len(msgs))
351+
for i, msg := range msgs {
352+
ids[i] = msg.Header.ID
353+
}
354+
return fb.In("id", ids)
355+
}

internal/events/persist_batch_test.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func TestPersistBatchContentSentByUsNotFoundFallback(t *testing.T) {
225225

226226
}
227227

228-
func TestPersistBatchContentSentByUsFoundMismatch(t *testing.T) {
228+
func TestPersistBatchContentSentByUsFoundDup(t *testing.T) {
229229

230230
em := newTestEventManager(t)
231231
defer em.cleanup(t)
@@ -234,21 +234,24 @@ func TestPersistBatchContentSentByUsFoundMismatch(t *testing.T) {
234234
batch := sampleBatch(t, core.BatchTypeBroadcast, core.TransactionTypeBatchPin, core.DataArray{data})
235235
batch.Node = testNodeID
236236

237+
msgID := batch.Payload.Messages[0].Header.ID
237238
em.mdm.On("GetMessageWithDataCached", em.ctx, batch.Payload.Messages[0].Header.ID).Return(&core.Message{
238239
Header: core.MessageHeader{
239-
ID: fftypes.NewUUID(),
240+
ID: msgID,
240241
},
241242
}, nil, true, nil)
242243

243244
em.mdi.On("InsertDataArray", mock.Anything, mock.Anything).Return(nil)
244245
em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimization miss"))
245-
em.mdi.On("UpsertMessage", mock.Anything, mock.Anything, database.UpsertOptimizationExisting, mock.AnythingOfType("database.PostCompletionHook")).Return(database.HashMismatch)
246+
em.mdi.On("GetMessageIDs", mock.Anything, "ns1", mock.Anything).Return([]*core.IDAndSequence{
247+
{ID: *msgID},
248+
}, nil)
246249

247250
em.mim.On("GetLocalNode", mock.Anything).Return(testNode, nil)
248251

249252
ok, err := em.persistBatchContent(em.ctx, batch, []*messageAndData{})
250253
assert.NoError(t, err)
251-
assert.False(t, ok)
254+
assert.True(t, ok)
252255

253256
}
254257

@@ -261,9 +264,10 @@ func TestPersistBatchContentInsertMessagesFail(t *testing.T) {
261264
batch := sampleBatch(t, core.BatchTypeBroadcast, core.TransactionTypeBatchPin, core.DataArray{data})
262265

263266
em.mdi.On("InsertDataArray", mock.Anything, mock.Anything).Return(nil)
264-
em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimization miss"))
265-
em.mdi.On("UpsertMessage", mock.Anything, mock.Anything, database.UpsertOptimizationExisting, mock.AnythingOfType("database.PostCompletionHook")).Return(nil).Run(func(args mock.Arguments) {
266-
args[3].(database.PostCompletionHook)()
267+
em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(fmt.Errorf("optimization miss")).Once()
268+
em.mdi.On("GetMessageIDs", mock.Anything, "ns1", mock.Anything).Return([]*core.IDAndSequence{}, nil)
269+
em.mdi.On("InsertMessages", mock.Anything, mock.Anything, mock.AnythingOfType("database.PostCompletionHook")).Return(nil).Once().Run(func(args mock.Arguments) {
270+
args[2].(database.PostCompletionHook)()
267271
})
268272

269273
msgData := &messageAndData{

0 commit comments

Comments
 (0)