Skip to content

Commit b7d08a8

Browse files
author
AKRAM@il.ibm.com
committed
Review comments
Signed-off-by: AKRAM@il.ibm.com <akram@akramb.vpc.cloud9.ibm.com>
1 parent abf8068 commit b7d08a8

3 files changed

Lines changed: 240 additions & 8 deletions

File tree

integration/token/fungible/views/auditor.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,9 @@ func (a *AuditView) Call(context view.Context) (interface{}, error) {
5757
// extract inputs and outputs
5858
logger.Debugf("AuditView: audit [%s]", tx.ID())
5959
inputs, outputs, err := auditor.Audit(context.Context(), tx)
60-
if err != nil {
61-
return nil, errors.Wrapf(err, "failed retrieving inputs and outputs")
62-
}
63-
defer auditor.Release(context.Context(), tx)
60+
assert.NoError(err, "failed retrieving inputs and outputs")
6461
logger.Debugf("AuditView: audit done [%s]", tx.ID())
62+
defer auditor.Release(context.Context(), tx)
6563

6664
logger.Debugf("AuditView: [%s] get query executor... ", tx.ID())
6765

integration/token/interop/views/auditor.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"math/big"
1212
"time"
1313

14-
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
1514
"github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/assert"
1615
"github.com/hyperledger-labs/fabric-smart-client/platform/view/view"
1716
"github.com/hyperledger-labs/fabric-token-sdk/token"
@@ -41,9 +40,7 @@ func (a *AuditView) Call(context view.Context) (interface{}, error) {
4140

4241
// extract inputs and outputs
4342
inputs, outputs, err := auditor.Audit(context.Context(), tx)
44-
if err != nil {
45-
return nil, errors.Wrapf(err, "failed retrieving inputs and outputs")
46-
}
43+
assert.NoError(err, "failed retrieving inputs and outputs")
4744
defer auditor.Release(context.Context(), tx)
4845

4946
// For example, all payments of an amount less than or equal to payment limit is valid

token/services/auditor/auditor_test.go

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,3 +719,240 @@ func TestManager_GetByTMSID(t *testing.T) {
719719
auditor.Get(sp, w)
720720
})
721721
}
722+
723+
// ---------------------------------------------------------------------------
724+
// Service.Audit Lock Management Tests
725+
// ---------------------------------------------------------------------------
726+
727+
// TestService_Audit_LocksReleasedOnAuditRecordError verifies that when Audit() fails
728+
// before lock acquisition (during AuditRecord()), no locks are held and Release() is safe.
729+
// Audit() acquires locks ONLY after successful AuditRecord(), so early failures don't leak locks.
730+
func TestService_Audit_LocksReleasedOnAuditRecordError(t *testing.T) {
731+
// Create a TMS that will fail on AuditRecord by returning nil public parameters
732+
mockTMS := &drivermock.TokenManagerService{}
733+
mockPPM := &drivermock.PublicParamsManager{}
734+
mockPPM.PublicParametersReturns(nil) // This will cause AuditRecord to fail
735+
mockTMS.PublicParamsManagerReturns(mockPPM)
736+
mockTMS.ValidatorReturns(&drivermock.Validator{}, nil)
737+
mockTMS.TokensServiceReturns(&drivermock.TokensService{})
738+
mockTMS.WalletServiceReturns(&drivermock.WalletService{})
739+
740+
mockVP := &tokenmock.VaultProvider{}
741+
mockV := &drivermock.Vault{}
742+
mockV.QueryEngineReturns(&drivermock.QueryEngine{})
743+
mockVP.VaultReturns(mockV, nil)
744+
745+
badTMS, err := token.NewManagementService(
746+
token.TMSID{}, mockTMS, logging.MustGetLogger("test"), mockVP, nil, nil,
747+
)
748+
require.NoError(t, err)
749+
750+
storeService := newTestStoreService(t, newFakeStore())
751+
svc := newTestService(storeService, nil)
752+
753+
tx := &auditmock.Transaction{}
754+
tx.IDReturns("tx-audit-record-err")
755+
tx.RequestReturns(token.NewRequest(badTMS, token.RequestAnchor("tx-audit-record-err")))
756+
757+
// Audit should fail
758+
_, _, err = svc.Audit(context.Background(), tx)
759+
require.Error(t, err)
760+
assert.Contains(t, err.Error(), "failed getting transaction audit record")
761+
762+
// Release should be safe to call even though Audit failed
763+
assert.NotPanics(t, func() {
764+
svc.Release(context.Background(), tx)
765+
})
766+
767+
// Verify no locks are held by trying to acquire the same anchor
768+
ctx := context.Background()
769+
err = storeService.AcquireLocks(ctx, "tx-audit-record-err")
770+
require.NoError(t, err, "should be able to acquire locks since Audit failed")
771+
storeService.ReleaseLocks(ctx, "tx-audit-record-err")
772+
}
773+
774+
// TestService_Audit_LocksAcquiredOnSuccess verifies successful Audit() acquires locks,
775+
// Release() frees them, and Release() is idempotent (safe to call multiple times).
776+
func TestService_Audit_LocksAcquiredOnSuccess(t *testing.T) {
777+
storeService := newTestStoreService(t, newFakeStore())
778+
svc := newTestService(storeService, nil)
779+
780+
tx := &auditmock.Transaction{}
781+
tx.IDReturns("tx-audit-success")
782+
tx.RequestReturns(token.NewRequest(newTestManagementService(t), token.RequestAnchor("tx-audit-success")))
783+
784+
ctx := context.Background()
785+
786+
// Audit should succeed
787+
inputs, outputs, err := svc.Audit(ctx, tx)
788+
require.NoError(t, err)
789+
assert.NotNil(t, inputs)
790+
assert.NotNil(t, outputs)
791+
792+
// Verify Release is safe to call
793+
assert.NotPanics(t, func() {
794+
svc.Release(ctx, tx)
795+
})
796+
797+
// Verify Release is idempotent
798+
assert.NotPanics(t, func() {
799+
svc.Release(ctx, tx)
800+
})
801+
}
802+
803+
// TestService_Audit_ContextCancellationBeforeLockAcquisition verifies context cancellation
804+
// doesn't leak locks. Semaphore auto-rolls back partially acquired locks (PR #1616).
805+
// Release() is always safe regardless of Audit() outcome.
806+
func TestService_Audit_ContextCancellationBeforeLockAcquisition(t *testing.T) {
807+
storeService := newTestStoreService(t, newFakeStore())
808+
svc := newTestService(storeService, nil)
809+
810+
tx := &auditmock.Transaction{}
811+
tx.IDReturns("tx-ctx-cancel")
812+
tx.RequestReturns(token.NewRequest(newTestManagementService(t), token.RequestAnchor("tx-ctx-cancel")))
813+
814+
// Use a cancelled context
815+
cancelledCtx, cancel := context.WithCancel(context.Background())
816+
cancel()
817+
818+
// Audit may fail due to context cancellation (depending on timing)
819+
// or succeed if AuditRecord completes before cancellation check
820+
_, _, _ = svc.Audit(cancelledCtx, tx)
821+
// We don't assert error here as it depends on timing
822+
823+
// Release should always be safe to call
824+
assert.NotPanics(t, func() {
825+
svc.Release(context.Background(), tx)
826+
})
827+
828+
// Verify we can acquire locks after (no locks were leaked)
829+
ctx := context.Background()
830+
err := storeService.AcquireLocks(ctx, "tx-ctx-cancel")
831+
require.NoError(t, err, "should be able to acquire locks")
832+
storeService.ReleaseLocks(ctx, "tx-ctx-cancel")
833+
}
834+
835+
// TestService_Audit_MultipleAuditsSequential verifies sequential audits work correctly:
836+
// first Audit() acquires locks, Release() frees them, second Audit() succeeds.
837+
func TestService_Audit_MultipleAuditsSequential(t *testing.T) {
838+
storeService := newTestStoreService(t, newFakeStore())
839+
svc := newTestService(storeService, nil)
840+
841+
ctx := context.Background()
842+
843+
// First audit
844+
tx1 := &auditmock.Transaction{}
845+
tx1.IDReturns("tx-audit-1")
846+
tx1.RequestReturns(token.NewRequest(newTestManagementService(t), token.RequestAnchor("tx-audit-1")))
847+
848+
inputs1, outputs1, err := svc.Audit(ctx, tx1)
849+
require.NoError(t, err)
850+
assert.NotNil(t, inputs1)
851+
assert.NotNil(t, outputs1)
852+
853+
// Release first audit's locks
854+
svc.Release(ctx, tx1)
855+
856+
// Second audit should succeed
857+
tx2 := &auditmock.Transaction{}
858+
tx2.IDReturns("tx-audit-2")
859+
tx2.RequestReturns(token.NewRequest(newTestManagementService(t), token.RequestAnchor("tx-audit-2")))
860+
861+
inputs2, outputs2, err := svc.Audit(ctx, tx2)
862+
require.NoError(t, err)
863+
assert.NotNil(t, inputs2)
864+
assert.NotNil(t, outputs2)
865+
866+
// Clean up
867+
svc.Release(ctx, tx2)
868+
}
869+
870+
// TestService_Audit_ReleaseIdempotency verifies Release() is idempotent - can be called
871+
// multiple times safely without panics (handles error paths, defer, retry logic).
872+
func TestService_Audit_ReleaseIdempotency(t *testing.T) {
873+
storeService := newTestStoreService(t, newFakeStore())
874+
svc := newTestService(storeService, nil)
875+
876+
tx := &auditmock.Transaction{}
877+
tx.IDReturns("tx-release-idempotent")
878+
tx.RequestReturns(token.NewRequest(newTestManagementService(t), token.RequestAnchor("tx-release-idempotent")))
879+
880+
// Audit to acquire locks
881+
_, _, err := svc.Audit(context.Background(), tx)
882+
require.NoError(t, err)
883+
884+
ctx := context.Background()
885+
886+
// First release should work
887+
assert.NotPanics(t, func() {
888+
svc.Release(ctx, tx)
889+
})
890+
891+
// Second release should also be safe (no-op)
892+
assert.NotPanics(t, func() {
893+
svc.Release(ctx, tx)
894+
})
895+
896+
// Third release should still be safe
897+
assert.NotPanics(t, func() {
898+
svc.Release(ctx, tx)
899+
})
900+
}
901+
902+
// TestService_Audit_ReleaseWithoutAudit verifies Release() is safe to call without
903+
// prior Audit() (handles defer in error paths where Audit() never ran or failed early).
904+
func TestService_Audit_ReleaseWithoutAudit(t *testing.T) {
905+
storeService := newTestStoreService(t, newFakeStore())
906+
svc := newTestService(storeService, nil)
907+
908+
tx := &auditmock.Transaction{}
909+
tx.IDReturns("tx-no-audit")
910+
tx.RequestReturns(token.NewRequest(newTestManagementService(t), token.RequestAnchor("tx-no-audit")))
911+
912+
// Release without Audit should be safe
913+
assert.NotPanics(t, func() {
914+
svc.Release(context.Background(), tx)
915+
})
916+
}
917+
918+
// TestService_Audit_PanicRecoveryReleasesLocks verifies defer Release() executes even
919+
// when code panics, preventing lock leaks. Demonstrates correct pattern:
920+
//
921+
// defer auditor.Release(ctx, tx) // MUST be after error check
922+
func TestService_Audit_PanicRecoveryReleasesLocks(t *testing.T) {
923+
storeService := newTestStoreService(t, newFakeStore())
924+
svc := newTestService(storeService, nil)
925+
926+
tx := &auditmock.Transaction{}
927+
tx.IDReturns("tx-panic-recovery")
928+
tx.RequestReturns(token.NewRequest(newTestManagementService(t), token.RequestAnchor("tx-panic-recovery")))
929+
930+
ctx := context.Background()
931+
932+
// Simulate code that panics after Audit but has defer Release
933+
func() {
934+
defer func() {
935+
if r := recover(); r != nil {
936+
// Panic recovered as expected
937+
assert.Equal(t, "simulated panic", r)
938+
}
939+
}()
940+
941+
// Audit succeeds and acquires locks
942+
inputs, outputs, err := svc.Audit(ctx, tx)
943+
require.NoError(t, err)
944+
assert.NotNil(t, inputs)
945+
assert.NotNil(t, outputs)
946+
947+
// Defer Release - this should execute even if panic occurs
948+
defer svc.Release(ctx, tx)
949+
950+
// Simulate panic in subsequent processing
951+
panic("simulated panic")
952+
}()
953+
954+
// Verify locks were released by attempting to acquire them
955+
err := storeService.AcquireLocks(ctx, "tx-panic-recovery")
956+
require.NoError(t, err, "locks should have been released despite panic")
957+
storeService.ReleaseLocks(ctx, "tx-panic-recovery")
958+
}

0 commit comments

Comments
 (0)