Skip to content

Commit

Permalink
chore: DeepCopy the operation object before retrying
Browse files Browse the repository at this point in the history
Signed-off-by: dwertent <[email protected]>
  • Loading branch information
dwertent committed Sep 6, 2024
1 parent aeec387 commit 0eadc21
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 7 deletions.
6 changes: 4 additions & 2 deletions internal/operations/manager.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Kaleido, Inc.
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand Down Expand Up @@ -234,10 +234,12 @@ func (om *operationsManager) RetryOperation(ctx context.Context, opID *fftypes.U
var po *core.PreparedOperation
var idempotencyKey core.IdempotencyKey
err = om.database.RunAsGroup(ctx, func(ctx context.Context) error {
op, err = om.findLatestRetry(ctx, opID)
parent, err := om.findLatestRetry(ctx, opID)
if err != nil {
return err
}
// Deep copy the operation so the parent ID will not get overwritten
op = parent.DeepCopy()

tx, err := om.updater.txHelper.GetTransactionByIDCached(ctx, op.Transaction)
if err != nil {
Expand Down
9 changes: 6 additions & 3 deletions internal/operations/manager_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2022 Kaleido, Inc.
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand Down Expand Up @@ -346,9 +346,12 @@ func TestRetryOperationSuccess(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 1, len(info.SetOperations))
assert.Equal(t, "retry", info.SetOperations[0].Field)
val, err := info.SetOperations[0].Value.Value()
retryVal, err := info.SetOperations[0].Value.Value()
assert.NoError(t, err)
assert.Equal(t, op.ID.String(), val)
// The retry value of the parent operation should be the new operation ID
assert.Equal(t, op.Retry.String(), retryVal.(string))
// The parent ID should not change
assert.Equal(t, op.ID.String(), opID.String())
return true
})).Return(true, nil)
mdi.On("GetTransactionByID", mock.Anything, "ns1", txID).Return(&core.Transaction{
Expand Down
75 changes: 74 additions & 1 deletion pkg/core/operation.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2023 Kaleido, Inc.
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand Down Expand Up @@ -74,6 +74,79 @@ func (op *Operation) IsTokenOperation() bool {
return op.Type == OpTypeTokenActivatePool || op.Type == OpTypeTokenApproval || op.Type == OpTypeTokenCreatePool || op.Type == OpTypeTokenTransfer
}

func (op *Operation) DeepCopy() *Operation {
cop := &Operation{
Namespace: op.Namespace,
Type: op.Type,
Status: op.Status,
Plugin: op.Plugin,
Error: op.Error,
}
if op.ID != nil {
idCopy := *op.ID
cop.ID = &idCopy
}
if op.Transaction != nil {
txCopy := *op.Transaction
cop.Transaction = &txCopy
}
if op.Created != nil {
createdCopy := *op.Created
cop.Created = &createdCopy
}
if op.Updated != nil {
updatedCopy := *op.Updated
cop.Updated = &updatedCopy
}
if op.Retry != nil {
retryCopy := *op.Retry
cop.Retry = &retryCopy
}
if op.Input != nil {
cop.Input = deepCopyMap(op.Input)
}
if op.Output != nil {
cop.Output = deepCopyMap(op.Output)
}
return cop
}

func deepCopyMap(original map[string]interface{}) map[string]interface{} {
if original == nil {
return nil
}
copy := make(map[string]interface{}, len(original))
for key, value := range original {
switch v := value.(type) {
case map[string]interface{}:
copy[key] = deepCopyMap(v)
case []interface{}:
copy[key] = deepCopySlice(v)
default:
copy[key] = v
}
}
return copy
}

func deepCopySlice(original []interface{}) []interface{} {
if original == nil {
return nil

Check warning on line 134 in pkg/core/operation.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/operation.go#L134

Added line #L134 was not covered by tests
}
copy := make([]interface{}, len(original))
for i, value := range original {
switch v := value.(type) {
case map[string]interface{}:
copy[i] = deepCopyMap(v)
case []interface{}:
copy[i] = deepCopySlice(v)

Check warning on line 142 in pkg/core/operation.go

View check run for this annotation

Codecov / codecov/patch

pkg/core/operation.go#L139-L142

Added lines #L139 - L142 were not covered by tests
default:
copy[i] = v
}
}
return copy
}

// OpStatus is the current status of an operation
type OpStatus string

Expand Down
117 changes: 116 additions & 1 deletion pkg/core/operation_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2021 Kaleido, Inc.
// Copyright © 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -18,6 +18,8 @@ package core

import (
"context"
"fmt"
"reflect"
"testing"

"github.com/hyperledger/firefly-common/pkg/fftypes"
Expand Down Expand Up @@ -97,6 +99,63 @@ func TestOperationTypes(t *testing.T) {
assert.False(t, op.IsBlockchainOperation())
}

func TestOperationDeepCopy(t *testing.T) {
op := &Operation{
ID: fftypes.NewUUID(),
Namespace: "ns1",
Transaction: fftypes.NewUUID(),
Type: OpTypeBlockchainInvoke,
Status: OpStatusInitialized,
Plugin: "fake",
Input: fftypes.JSONObject{"key": "value"},
Output: fftypes.JSONObject{"result": "success"},
Error: "error message",
Created: fftypes.Now(),
Updated: fftypes.Now(),
Retry: fftypes.NewUUID(),
}

copyOp := op.DeepCopy()
shallowCopy := op // Shallow copy for showcasing that DeepCopy is a deep copy

// Ensure the data was copied correctly
assert.Equal(t, op.ID, copyOp.ID)
assert.Equal(t, op.Namespace, copyOp.Namespace)
assert.Equal(t, op.Transaction, copyOp.Transaction)
assert.Equal(t, op.Type, copyOp.Type)
assert.Equal(t, op.Status, copyOp.Status)
assert.Equal(t, op.Plugin, copyOp.Plugin)
assert.Equal(t, op.Input, copyOp.Input)
assert.Equal(t, op.Output, copyOp.Output)
assert.Equal(t, op.Error, copyOp.Error)
assert.Equal(t, op.Created, copyOp.Created)
assert.Equal(t, op.Updated, copyOp.Updated)
assert.Equal(t, op.Retry, copyOp.Retry)

// Modify the original and ensure the copy is not modified
*op.ID = *fftypes.NewUUID()
assert.NotEqual(t, copyOp.ID, op.ID)

*op.Created = *fftypes.Now()
assert.NotEqual(t, copyOp.Created, op.Created)

// Ensure the copy is a deep copy by comparing the pointers of the fields
assert.NotEqual(t, fmt.Sprintf("%p", copyOp.ID), fmt.Sprintf("%p", op.ID))
assert.NotEqual(t, fmt.Sprintf("%p", copyOp.Created), fmt.Sprintf("%p", op.Created))
assert.NotEqual(t, fmt.Sprintf("%p", copyOp.Updated), fmt.Sprintf("%p", op.Updated))
assert.NotEqual(t, fmt.Sprintf("%p", copyOp.Transaction), fmt.Sprintf("%p", op.Transaction))
assert.NotEqual(t, fmt.Sprintf("%p", copyOp.Retry), fmt.Sprintf("%p", op.Retry))
assert.NotEqual(t, fmt.Sprintf("%p", copyOp.Input), fmt.Sprintf("%p", op.Input))
assert.NotEqual(t, fmt.Sprintf("%p", copyOp.Output), fmt.Sprintf("%p", op.Output))

// 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
assert.Equal(t, shallowCopy.ID, op.ID)
assert.Equal(t, fmt.Sprintf("%p", shallowCopy.ID), fmt.Sprintf("%p", op.ID))

// Ensure no new fields are added to the Operation struct
// If a new field is added, this test will fail and the DeepCopy function should be updated
assert.Equal(t, 12, reflect.TypeOf(Operation{}).NumField())
}
func TestParseNamespacedOpID(t *testing.T) {

ctx := context.Background()
Expand Down Expand Up @@ -124,3 +183,59 @@ func TestParseNamespacedOpID(t *testing.T) {
assert.Equal(t, "ns1", ns)

}

func TestDeepCopyMapNil(t *testing.T) {
original := map[string]interface{}(nil)
copy := deepCopyMap(original)
assert.Nil(t, copy)
}

func TestDeepCopyMapEmpty(t *testing.T) {
original := map[string]interface{}{}
copy := deepCopyMap(original)
assert.NotNil(t, copy)
assert.Empty(t, copy)
}

func TestDeepCopyMapSimple(t *testing.T) {
original := map[string]interface{}{
"key1": "value1",
"key2": 42,
}
copy := deepCopyMap(original)
assert.Equal(t, original, copy)
}

func TestDeepCopyMapNestedMap(t *testing.T) {
original := map[string]interface{}{
"key1": map[string]interface{}{
"nestedKey1": "nestedValue1",
},
}
copy := deepCopyMap(original)
assert.Equal(t, original, copy)
assert.NotSame(t, original["key1"], copy["key1"])
}

func TestDeepCopyMapNestedSlice(t *testing.T) {
original := map[string]interface{}{
"key1": []interface{}{"value1", 42},
}
copy := deepCopyMap(original)
assert.Equal(t, original, copy)
assert.NotSame(t, original["key1"], copy["key1"])
}

func TestDeepCopyMapMixed(t *testing.T) {
original := map[string]interface{}{
"key1": "value1",
"key2": map[string]interface{}{
"nestedKey1": "nestedValue1",
},
"key3": []interface{}{"value1", 42},
}
copy := deepCopyMap(original)
assert.Equal(t, original, copy)
assert.NotSame(t, original["key2"], copy["key2"])
assert.NotSame(t, original["key3"], copy["key3"])
}

0 comments on commit 0eadc21

Please sign in to comment.