Skip to content

Commit af47a2e

Browse files
authored
Merge pull request #226 from NDDev-OpenNetwork/fix/recovery-lock-scope-20261009-signed
fix(recovery): match write-set locks to repository plans
2 parents f55794e + 13cb1b3 commit af47a2e

4 files changed

Lines changed: 42 additions & 8 deletions

File tree

‎.gds/bundle.lock.yaml‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ schema_version: 1
44
bundle:
55
version: "0.9.7-dev"
66
release_sequence: 0
7-
source_tree_digest: "sha256:4c7e5f20f939b7c78e0d3432c61d62f0b01d7fade56621491da307d656463448"
8-
digest: "sha256:3b2e0e5abf1515b37407f609e7e35034136c5acea5ad46f37c67aac17f3ed93a"
7+
source_tree_digest: "sha256:4a2f83f369d3820571e9572382e50436b38e1f3057a4549814c6a24ef7432e81"
8+
digest: "sha256:85055636230aad2c7e7cc9e15f53c42cd64cc356c889048806a12d98bd7bfd07"
99

1010
projection:
11-
input_digest: "sha256:924e25d3b7a56d3966975e735ec39f9eec2bca3420b0d4ee6af6e6e07d5191e0"
12-
output_digest: "sha256:91c917a3497b68e1c5d6ab372a072a43f0e34f4815511b341357e5b0721b04aa"
11+
input_digest: "sha256:28ac88ff134bfafe0c1c65d0537987899c2616a49a71e7d340cdf245611b6cf8"
12+
output_digest: "sha256:ef44e3e22510e72e0062ec6593de83ecc3b43cc4d52a8ef7858383491ea1af8b"
1313
files:
1414
- path: ".gds/compiled-policy.json"
1515
digest: "sha256:9f498788bdc34e52a0ab793c536e0e6a7b360c2e1a20446cbf03ed51986cdc6f"
1616
- path: ".github/workflows/gds-ci.yml"
17-
digest: "sha256:4f263ba0c7c0e71d2eaeacca25f1fbdaa43cac912d12a178b00dee26c051f932"
17+
digest: "sha256:cb471c7e0af94c79ca2912bf347fb400e60fddf1aacfcd229837524806053749"

‎.github/workflows/gds-ci.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# GENERATED FILE - DO NOT EDIT DIRECTLY
22
# generator: gds
33
# bundle: 0.9.7-dev
4-
# source-tree-digest: sha256:4c7e5f20f939b7c78e0d3432c61d62f0b01d7fade56621491da307d656463448
5-
# input-digest: sha256:924e25d3b7a56d3966975e735ec39f9eec2bca3420b0d4ee6af6e6e07d5191e0
4+
# source-tree-digest: sha256:4a2f83f369d3820571e9572382e50436b38e1f3057a4549814c6a24ef7432e81
5+
# input-digest: sha256:28ac88ff134bfafe0c1c65d0537987899c2616a49a71e7d340cdf245611b6cf8
66
# output-digest: sha256:4ef1ee2fcc42927eaedef9c85f7b421f87e5cc75ff7055ef520216a00f7d4b74
77
# edit-source:
88
# - .gds/repository.yaml

‎core/app/recovery_operations.go‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ func decideRecovery(
556556
if lock.Scope != "repository" {
557557
decision.Blockers = append(decision.Blockers, "unexpected-lock-scope")
558558
}
559-
if _, allowed := allowedRepositories[lock.ScopeID]; !allowed {
559+
if !repositoryLockBelongsToPlan(lock.ScopeID, allowedRepositories) {
560560
decision.Blockers = append(decision.Blockers, "lock-scope-outside-plan")
561561
}
562562
if lock.DeviceID != deviceID {
@@ -655,6 +655,24 @@ func decideRecovery(
655655
return decision
656656
}
657657

658+
// repositoryLockBelongsToPlan accepts both the legacy bare repository scope
659+
// and the operation engine's write-set scope (`<repository-id>:<target>`).
660+
// Locks are keyed by the latter so independent write targets in one repository
661+
// do not collide, while recovery is scoped by the repository id in the plan.
662+
// Comparing only exact strings made every interrupted write-set operation look
663+
// foreign and left its expired lock unrecoverable.
664+
func repositoryLockBelongsToPlan(scopeID string, allowedRepositories map[string]struct{}) bool {
665+
if _, allowed := allowedRepositories[scopeID]; allowed {
666+
return true
667+
}
668+
for repositoryID := range allowedRepositories {
669+
if strings.HasPrefix(scopeID, repositoryID+":") {
670+
return true
671+
}
672+
}
673+
return false
674+
}
675+
658676
func processAlive(pid int) (bool, error) {
659677
if pid <= 0 {
660678
return false, fmt.Errorf("invalid process id %d", pid)

‎core/app/recovery_operations_test.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ func TestRecoveryDecisionReportsCompensationWithoutApplyingIt(t *testing.T) {
7373
}
7474
}
7575

76+
func TestRecoveryDecisionAcceptsWriteSetRepositoryLockScope(t *testing.T) {
77+
now := time.Now().UTC()
78+
snapshot := recoveryDecisionFixture(now)
79+
snapshot.Locks[0].ScopeID = "repo_fixture:repository"
80+
plan := operations.Plan{
81+
Steps: []operations.Step{{
82+
StepID: "step-1", Compensation: operations.Compensation{Mode: "manual"},
83+
}},
84+
Scope: operations.Scope{Repositories: []string{"repo_fixture"}},
85+
}
86+
decision := decideRecovery(snapshot, plan, now, "device:test")
87+
if !decision.Automatable || decision.Classification != "safe-abort" {
88+
t.Fatalf("decision=%#v", decision)
89+
}
90+
}
91+
7692
func containsString(values []string, expected string) bool {
7793
for _, value := range values {
7894
if value == expected {

0 commit comments

Comments
 (0)