|
1 | 1 | import { afterEach, describe, expect, it, vi } from "vitest"; |
| 2 | +import { heldLockCountForTest, releaseAllHeldLocksAtShutdown } from "../../src/queue/held-lock-registry"; |
2 | 3 | import { SubmissionLock } from "../../src/queue/submission-lock"; |
3 | 4 | import { |
4 | 5 | claimContributorCapLock, |
@@ -501,15 +502,18 @@ describe("domain wrappers + PrActuationLockContendedError (#8896)", () => { |
501 | 502 | }); |
502 | 503 | delete env.SUBMISSION_LOCK; |
503 | 504 |
|
504 | | - await claimPrActuationLock(env, "acme/widgets", 7); |
| 505 | + const prClaim = await claimPrActuationLock(env, "acme/widgets", 7); |
505 | 506 | const [actuationTtl] = ttlCalls; |
506 | 507 | ttlCalls.length = 0; |
507 | 508 |
|
508 | | - await claimContributorCapLock(env, "acme/widgets", "alice"); |
| 509 | + const capClaim = await claimContributorCapLock(env, "acme/widgets", "alice"); |
509 | 510 | const [capTtl] = ttlCalls; |
510 | 511 |
|
511 | 512 | expect(capTtl).toBe(actuationTtl); |
512 | 513 | expect(capTtl).toBe(600); |
| 514 | + |
| 515 | + await releasePrActuationLock(env, "acme/widgets", 7, prClaim.ownerToken); |
| 516 | + await releaseContributorCapLock(env, "acme/widgets", "alice", capClaim.ownerToken); |
513 | 517 | }); |
514 | 518 |
|
515 | 519 | it("builds a fast-retry contended error with a distinct retryKind", () => { |
@@ -841,3 +845,152 @@ describe("lock heartbeat end-to-end (#9467)", () => { |
841 | 845 | expect((await claimTransientLock(env, "lock:pr", 60)).acquired).toBe(true); |
842 | 846 | }); |
843 | 847 | }); |
| 848 | + |
| 849 | +// #10021: register actuation and contributor-cap locks in the shutdown held-lock registry (same shape as |
| 850 | +// claimAiReviewLock / releaseAiReviewLock in ai-review-orchestration.ts). |
| 851 | +describe("claimPrActuationLock / releasePrActuationLock register with the held-lock registry (#10021)", () => { |
| 852 | + afterEach(async () => { |
| 853 | + await releaseAllHeldLocksAtShutdown(); |
| 854 | + }); |
| 855 | + |
| 856 | + function cacheWithReleaseTracking() { |
| 857 | + const releases: Array<{ key: string; value: string }> = []; |
| 858 | + const held = new Map<string, string>(); |
| 859 | + const cache = { |
| 860 | + get: async (key: string) => held.get(key) ?? null, |
| 861 | + set: async () => undefined, |
| 862 | + claim: async (key: string, value: string) => { |
| 863 | + if (held.has(key)) return false; |
| 864 | + held.set(key, value); |
| 865 | + return true; |
| 866 | + }, |
| 867 | + releaseIfValue: async (key: string, value: string) => { |
| 868 | + releases.push({ key, value }); |
| 869 | + if (held.get(key) !== value) return false; |
| 870 | + held.delete(key); |
| 871 | + return true; |
| 872 | + }, |
| 873 | + }; |
| 874 | + return { cache, releases }; |
| 875 | + } |
| 876 | + |
| 877 | + it("registers a real claim, and shutdown release issues releaseIfValue for the pr-actuation-lock key", async () => { |
| 878 | + const { cache, releases } = cacheWithReleaseTracking(); |
| 879 | + const env = createTestEnv({ SELFHOST_TRANSIENT_CACHE: cache }); |
| 880 | + delete env.SUBMISSION_LOCK; |
| 881 | + |
| 882 | + const before = heldLockCountForTest(); |
| 883 | + const claim = await claimPrActuationLock(env, "acme/widgets", 7); |
| 884 | + expect(claim.acquired).toBe(true); |
| 885 | + expect(heldLockCountForTest()).toBe(before + 1); |
| 886 | + expect(releases).toHaveLength(0); |
| 887 | + |
| 888 | + expect(await releaseAllHeldLocksAtShutdown()).toBe(before + 1); |
| 889 | + expect(releases).toEqual([ |
| 890 | + { key: "pr-actuation-lock:acme/widgets#7", value: claim.ownerToken }, |
| 891 | + ]); |
| 892 | + }); |
| 893 | + |
| 894 | + it("unregisters on the normal release path with a matching owner token", async () => { |
| 895 | + const env = createTestEnv(); |
| 896 | + const before = heldLockCountForTest(); |
| 897 | + const claim = await claimPrActuationLock(env, "acme/widgets", 8); |
| 898 | + await releasePrActuationLock(env, "acme/widgets", 8, claim.ownerToken); |
| 899 | + expect(heldLockCountForTest()).toBe(before); |
| 900 | + }); |
| 901 | + |
| 902 | + it("does not register a fail-open claim — there is nothing real to release", async () => { |
| 903 | + const env = createTestEnv(); |
| 904 | + delete (env as { SELFHOST_TRANSIENT_CACHE?: unknown }).SELFHOST_TRANSIENT_CACHE; |
| 905 | + const before = heldLockCountForTest(); |
| 906 | + const claim = await claimPrActuationLock(env, "acme/widgets", 10); |
| 907 | + expect(claim.ownerToken).toBeNull(); |
| 908 | + expect(heldLockCountForTest()).toBe(before); |
| 909 | + }); |
| 910 | + |
| 911 | + it("a null-token release does not unregister a real held entry", async () => { |
| 912 | + const env = createTestEnv(); |
| 913 | + const before = heldLockCountForTest(); |
| 914 | + await claimPrActuationLock(env, "acme/widgets", 11); |
| 915 | + await releasePrActuationLock(env, "acme/widgets", 11, null); |
| 916 | + expect(heldLockCountForTest()).toBe(before + 1); |
| 917 | + }); |
| 918 | + |
| 919 | + it("#10021 releasePrActuationLock with a non-matching ownerToken does not evict the registry entry (#9468)", async () => { |
| 920 | + const env = createTestEnv(); |
| 921 | + const before = heldLockCountForTest(); |
| 922 | + await claimPrActuationLock(env, "acme/widgets", 9); |
| 923 | + expect(heldLockCountForTest()).toBe(before + 1); |
| 924 | + await releasePrActuationLock(env, "acme/widgets", 9, "tok-someone-else"); |
| 925 | + expect(heldLockCountForTest()).toBe(before + 1); |
| 926 | + }); |
| 927 | +}); |
| 928 | + |
| 929 | +describe("claimContributorCapLock / releaseContributorCapLock register with the held-lock registry (#10021)", () => { |
| 930 | + afterEach(async () => { |
| 931 | + await releaseAllHeldLocksAtShutdown(); |
| 932 | + }); |
| 933 | + |
| 934 | + function cacheWithReleaseTracking() { |
| 935 | + const releases: Array<{ key: string; value: string }> = []; |
| 936 | + const held = new Map<string, string>(); |
| 937 | + const cache = { |
| 938 | + get: async (key: string) => held.get(key) ?? null, |
| 939 | + set: async () => undefined, |
| 940 | + claim: async (key: string, value: string) => { |
| 941 | + if (held.has(key)) return false; |
| 942 | + held.set(key, value); |
| 943 | + return true; |
| 944 | + }, |
| 945 | + releaseIfValue: async (key: string, value: string) => { |
| 946 | + releases.push({ key, value }); |
| 947 | + if (held.get(key) !== value) return false; |
| 948 | + held.delete(key); |
| 949 | + return true; |
| 950 | + }, |
| 951 | + }; |
| 952 | + return { cache, releases }; |
| 953 | + } |
| 954 | + |
| 955 | + it("registers a real claim, and shutdown release issues releaseIfValue for the contributor-cap-lock key", async () => { |
| 956 | + const { cache, releases } = cacheWithReleaseTracking(); |
| 957 | + const env = createTestEnv({ SELFHOST_TRANSIENT_CACHE: cache }); |
| 958 | + delete env.SUBMISSION_LOCK; |
| 959 | + |
| 960 | + const before = heldLockCountForTest(); |
| 961 | + const claim = await claimContributorCapLock(env, "Acme/Widgets", "Alice"); |
| 962 | + expect(claim.acquired).toBe(true); |
| 963 | + expect(heldLockCountForTest()).toBe(before + 1); |
| 964 | + expect(releases).toHaveLength(0); |
| 965 | + |
| 966 | + expect(await releaseAllHeldLocksAtShutdown()).toBe(before + 1); |
| 967 | + expect(releases).toEqual([ |
| 968 | + { key: "contributor-cap-lock:acme/widgets:alice", value: claim.ownerToken }, |
| 969 | + ]); |
| 970 | + }); |
| 971 | + |
| 972 | + it("unregisters on the normal release path with a matching owner token", async () => { |
| 973 | + const env = createTestEnv(); |
| 974 | + const before = heldLockCountForTest(); |
| 975 | + const claim = await claimContributorCapLock(env, "acme/widgets", "bob"); |
| 976 | + await releaseContributorCapLock(env, "acme/widgets", "bob", claim.ownerToken); |
| 977 | + expect(heldLockCountForTest()).toBe(before); |
| 978 | + }); |
| 979 | + |
| 980 | + it("does not register a fail-open claim — there is nothing real to release", async () => { |
| 981 | + const env = createTestEnv(); |
| 982 | + delete (env as { SELFHOST_TRANSIENT_CACHE?: unknown }).SELFHOST_TRANSIENT_CACHE; |
| 983 | + const before = heldLockCountForTest(); |
| 984 | + const claim = await claimContributorCapLock(env, "acme/widgets", "dave"); |
| 985 | + expect(claim.ownerToken).toBeNull(); |
| 986 | + expect(heldLockCountForTest()).toBe(before); |
| 987 | + }); |
| 988 | + |
| 989 | + it("a null-token release does not unregister a real held entry", async () => { |
| 990 | + const env = createTestEnv(); |
| 991 | + const before = heldLockCountForTest(); |
| 992 | + await claimContributorCapLock(env, "acme/widgets", "carol"); |
| 993 | + await releaseContributorCapLock(env, "acme/widgets", "carol", null); |
| 994 | + expect(heldLockCountForTest()).toBe(before + 1); |
| 995 | + }); |
| 996 | +}); |
0 commit comments