Skip to content

Commit 86c52e8

Browse files
authored
Merge pull request #1561 from dwertent/fix-retry-operations
chore: Fix Cache Mismatch by Deep Copying Operation
2 parents 0ab3df9 + 2075473 commit 86c52e8

File tree

4 files changed

+253
-7
lines changed

4 files changed

+253
-7
lines changed

internal/operations/manager.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2023 Kaleido, Inc.
1+
// Copyright © 2024 Kaleido, Inc.
22
//
33
// SPDX-License-Identifier: Apache-2.0
44
//
@@ -234,10 +234,12 @@ func (om *operationsManager) RetryOperation(ctx context.Context, opID *fftypes.U
234234
var po *core.PreparedOperation
235235
var idempotencyKey core.IdempotencyKey
236236
err = om.database.RunAsGroup(ctx, func(ctx context.Context) error {
237-
op, err = om.findLatestRetry(ctx, opID)
237+
parent, err := om.findLatestRetry(ctx, opID)
238238
if err != nil {
239239
return err
240240
}
241+
// Deep copy the operation so the parent ID will not get overwritten
242+
op = parent.DeepCopy()
241243

242244
tx, err := om.updater.txHelper.GetTransactionByIDCached(ctx, op.Transaction)
243245
if err != nil {

internal/operations/manager_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2022 Kaleido, Inc.
1+
// Copyright © 2024 Kaleido, Inc.
22
//
33
// SPDX-License-Identifier: Apache-2.0
44
//
@@ -346,9 +346,12 @@ func TestRetryOperationSuccess(t *testing.T) {
346346
assert.NoError(t, err)
347347
assert.Equal(t, 1, len(info.SetOperations))
348348
assert.Equal(t, "retry", info.SetOperations[0].Field)
349-
val, err := info.SetOperations[0].Value.Value()
349+
retryVal, err := info.SetOperations[0].Value.Value()
350350
assert.NoError(t, err)
351-
assert.Equal(t, op.ID.String(), val)
351+
// The retry value of the parent operation should be the new operation ID
352+
assert.Equal(t, op.Retry.String(), retryVal.(string))
353+
// The parent ID should not change
354+
assert.Equal(t, op.ID.String(), opID.String())
352355
return true
353356
})).Return(true, nil)
354357
mdi.On("GetTransactionByID", mock.Anything, "ns1", txID).Return(&core.Transaction{

pkg/core/operation.go

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2023 Kaleido, Inc.
1+
// Copyright © 2024 Kaleido, Inc.
22
//
33
// SPDX-License-Identifier: Apache-2.0
44
//
@@ -74,6 +74,79 @@ func (op *Operation) IsTokenOperation() bool {
7474
return op.Type == OpTypeTokenActivatePool || op.Type == OpTypeTokenApproval || op.Type == OpTypeTokenCreatePool || op.Type == OpTypeTokenTransfer
7575
}
7676

77+
func (op *Operation) DeepCopy() *Operation {
78+
cop := &Operation{
79+
Namespace: op.Namespace,
80+
Type: op.Type,
81+
Status: op.Status,
82+
Plugin: op.Plugin,
83+
Error: op.Error,
84+
}
85+
if op.ID != nil {
86+
idCopy := *op.ID
87+
cop.ID = &idCopy
88+
}
89+
if op.Transaction != nil {
90+
txCopy := *op.Transaction
91+
cop.Transaction = &txCopy
92+
}
93+
if op.Created != nil {
94+
createdCopy := *op.Created
95+
cop.Created = &createdCopy
96+
}
97+
if op.Updated != nil {
98+
updatedCopy := *op.Updated
99+
cop.Updated = &updatedCopy
100+
}
101+
if op.Retry != nil {
102+
retryCopy := *op.Retry
103+
cop.Retry = &retryCopy
104+
}
105+
if op.Input != nil {
106+
cop.Input = deepCopyMap(op.Input)
107+
}
108+
if op.Output != nil {
109+
cop.Output = deepCopyMap(op.Output)
110+
}
111+
return cop
112+
}
113+
114+
func deepCopyMap(original map[string]interface{}) map[string]interface{} {
115+
if original == nil {
116+
return nil
117+
}
118+
copy := make(map[string]interface{}, len(original))
119+
for key, value := range original {
120+
switch v := value.(type) {
121+
case map[string]interface{}:
122+
copy[key] = deepCopyMap(v)
123+
case []interface{}:
124+
copy[key] = deepCopySlice(v)
125+
default:
126+
copy[key] = v
127+
}
128+
}
129+
return copy
130+
}
131+
132+
func deepCopySlice(original []interface{}) []interface{} {
133+
if original == nil {
134+
return nil
135+
}
136+
copy := make([]interface{}, len(original))
137+
for i, value := range original {
138+
switch v := value.(type) {
139+
case map[string]interface{}:
140+
copy[i] = deepCopyMap(v)
141+
case []interface{}:
142+
copy[i] = deepCopySlice(v)
143+
default:
144+
copy[i] = v
145+
}
146+
}
147+
return copy
148+
}
149+
77150
// OpStatus is the current status of an operation
78151
type OpStatus string
79152

pkg/core/operation_test.go

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2021 Kaleido, Inc.
1+
// Copyright © 2024 Kaleido, Inc.
22
//
33
// SPDX-License-Identifier: Apache-2.0
44
//
@@ -18,6 +18,7 @@ package core
1818

1919
import (
2020
"context"
21+
"reflect"
2122
"testing"
2223

2324
"github.com/hyperledger/firefly-common/pkg/fftypes"
@@ -97,6 +98,63 @@ func TestOperationTypes(t *testing.T) {
9798
assert.False(t, op.IsBlockchainOperation())
9899
}
99100

101+
func TestOperationDeepCopy(t *testing.T) {
102+
op := &Operation{
103+
ID: fftypes.NewUUID(),
104+
Namespace: "ns1",
105+
Transaction: fftypes.NewUUID(),
106+
Type: OpTypeBlockchainInvoke,
107+
Status: OpStatusInitialized,
108+
Plugin: "fake",
109+
Input: fftypes.JSONObject{"key": "value"},
110+
Output: fftypes.JSONObject{"result": "success"},
111+
Error: "error message",
112+
Created: fftypes.Now(),
113+
Updated: fftypes.Now(),
114+
Retry: fftypes.NewUUID(),
115+
}
116+
117+
copyOp := op.DeepCopy()
118+
shallowCopy := op // Shallow copy for showcasing that DeepCopy is a deep copy
119+
120+
// Ensure the data was copied correctly
121+
assert.Equal(t, op.ID, copyOp.ID)
122+
assert.Equal(t, op.Namespace, copyOp.Namespace)
123+
assert.Equal(t, op.Transaction, copyOp.Transaction)
124+
assert.Equal(t, op.Type, copyOp.Type)
125+
assert.Equal(t, op.Status, copyOp.Status)
126+
assert.Equal(t, op.Plugin, copyOp.Plugin)
127+
assert.Equal(t, op.Input, copyOp.Input)
128+
assert.Equal(t, op.Output, copyOp.Output)
129+
assert.Equal(t, op.Error, copyOp.Error)
130+
assert.Equal(t, op.Created, copyOp.Created)
131+
assert.Equal(t, op.Updated, copyOp.Updated)
132+
assert.Equal(t, op.Retry, copyOp.Retry)
133+
134+
// Modify the original and ensure the copy is not modified
135+
*op.ID = *fftypes.NewUUID()
136+
assert.NotEqual(t, copyOp.ID, op.ID)
137+
138+
*op.Created = *fftypes.Now()
139+
assert.NotEqual(t, copyOp.Created, op.Created)
140+
141+
// Ensure the copy is a deep copy by comparing the pointers of the fields
142+
assert.NotSame(t, copyOp.ID, op.ID)
143+
assert.NotSame(t, copyOp.Created, op.Created)
144+
assert.NotSame(t, copyOp.Updated, op.Updated)
145+
assert.NotSame(t, copyOp.Transaction, op.Transaction)
146+
assert.NotSame(t, copyOp.Retry, op.Retry)
147+
assert.NotSame(t, copyOp.Input, op.Input)
148+
assert.NotSame(t, copyOp.Output, op.Output)
149+
150+
// showcasing that the shallow copy is a shallow copy and the copied object value changed as well the pointer has the same address as the original
151+
assert.Equal(t, shallowCopy.ID, op.ID)
152+
assert.Same(t, shallowCopy.ID, op.ID)
153+
154+
// Ensure no new fields are added to the Operation struct
155+
// If a new field is added, this test will fail and the DeepCopy function should be updated
156+
assert.Equal(t, 12, reflect.TypeOf(Operation{}).NumField())
157+
}
100158
func TestParseNamespacedOpID(t *testing.T) {
101159

102160
ctx := context.Background()
@@ -124,3 +182,113 @@ func TestParseNamespacedOpID(t *testing.T) {
124182
assert.Equal(t, "ns1", ns)
125183

126184
}
185+
186+
func TestDeepCopyMapNil(t *testing.T) {
187+
original := map[string]interface{}(nil)
188+
copy := deepCopyMap(original)
189+
assert.Nil(t, copy)
190+
}
191+
192+
func TestDeepCopyMapEmpty(t *testing.T) {
193+
original := map[string]interface{}{}
194+
copy := deepCopyMap(original)
195+
assert.NotNil(t, copy)
196+
assert.Empty(t, copy)
197+
}
198+
199+
func TestDeepCopyMapSimple(t *testing.T) {
200+
original := map[string]interface{}{
201+
"key1": "value1",
202+
"key2": 42,
203+
}
204+
copy := deepCopyMap(original)
205+
assert.Equal(t, original, copy)
206+
}
207+
208+
func TestDeepCopyMapNestedMap(t *testing.T) {
209+
original := map[string]interface{}{
210+
"key1": map[string]interface{}{
211+
"nestedKey1": "nestedValue1",
212+
},
213+
}
214+
copy := deepCopyMap(original)
215+
assert.Equal(t, original, copy)
216+
assert.NotSame(t, original["key1"], copy["key1"])
217+
}
218+
219+
func TestDeepCopyMapNestedSlice(t *testing.T) {
220+
original := map[string]interface{}{
221+
"key1": []interface{}{"value1", 42},
222+
}
223+
copy := deepCopyMap(original)
224+
assert.Equal(t, original, copy)
225+
assert.NotSame(t, original["key1"], copy["key1"])
226+
}
227+
228+
func TestDeepCopyMapMixed(t *testing.T) {
229+
original := map[string]interface{}{
230+
"key1": "value1",
231+
"key2": map[string]interface{}{
232+
"nestedKey1": "nestedValue1",
233+
},
234+
"key3": []interface{}{"value1", 42},
235+
}
236+
copy := deepCopyMap(original)
237+
assert.Equal(t, original, copy)
238+
assert.NotSame(t, original["key2"], copy["key2"])
239+
assert.NotSame(t, original["key3"], copy["key3"])
240+
}
241+
242+
func TestDeepCopySliceNil(t *testing.T) {
243+
original := []interface{}(nil)
244+
copy := deepCopySlice(original)
245+
assert.Nil(t, copy)
246+
}
247+
248+
func TestDeepCopySliceEmpty(t *testing.T) {
249+
original := []interface{}{}
250+
copy := deepCopySlice(original)
251+
assert.NotNil(t, copy)
252+
assert.Empty(t, copy)
253+
}
254+
255+
func TestDeepCopySliceSimple(t *testing.T) {
256+
original := []interface{}{"value1", 42}
257+
copy := deepCopySlice(original)
258+
assert.Equal(t, original, copy)
259+
}
260+
261+
func TestDeepCopySliceNestedMap(t *testing.T) {
262+
original := []interface{}{
263+
map[string]interface{}{
264+
"nestedKey1": "nestedValue1",
265+
},
266+
}
267+
copy := deepCopySlice(original)
268+
assert.Equal(t, original, copy)
269+
assert.NotSame(t, original[0], copy[0])
270+
}
271+
272+
func TestDeepCopySliceNestedSlice(t *testing.T) {
273+
original := []interface{}{
274+
[]interface{}{"value1", 42},
275+
}
276+
copy := deepCopySlice(original)
277+
assert.Equal(t, original, copy)
278+
assert.NotSame(t, original[0], copy[0])
279+
}
280+
281+
func TestDeepCopySliceMixed(t *testing.T) {
282+
original := []interface{}{
283+
"value1",
284+
42,
285+
map[string]interface{}{
286+
"nestedKey1": "nestedValue1",
287+
},
288+
[]interface{}{"value2", 43},
289+
}
290+
copy := deepCopySlice(original)
291+
assert.Equal(t, original, copy)
292+
assert.NotSame(t, original[2], copy[2])
293+
assert.NotSame(t, original[3], copy[3])
294+
}

0 commit comments

Comments
 (0)