Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions infra/relay/src/environments/ManagedEndpointProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1534,6 +1534,36 @@ describe("ManagedEndpointProvider", () => {
}).pipe(Effect.provide(layer));
});

it.effect("reports a tunnel with an attached connector as not released", () => {
const tunnelClient = ManagedEndpointProvider.ManagedEndpointTunnelClient.of({
...makeTunnelClient(),
delete: (tunnelId) =>
Effect.fail(
new ManagedEndpointProvider.ManagedEndpointTunnelClientError({
operation: "delete",
tunnelId,
cause: {
_tag: "BadRequest",
code: 1022,
message:
"This tunnel has active connections. Please stop all cloudflared replicas, or wait a few minutes for connections to close, then try again.",
},
}),
),
});
const layer = providerLayer(tunnelClient, makeDnsClient(), makeAllocations());

return Effect.gen(function* () {
const provider = yield* ManagedEndpointProvider.ManagedEndpointProvider;
const key = { userId: "user_ABC", environmentId: "env_ABC" } as const;
yield* provider.provision({
...key,
origin: { localHttpHost: "127.0.0.1", localHttpPort: 3773 },
});
expect(yield* provider.release(key)).toBe(false);
}).pipe(Effect.provide(layer));
});

it.effect("treats an absent allocation as already deprovisioned", () => {
const tunnelCalls: TunnelCall[] = [];
const dnsCalls: DnsCall[] = [];
Expand Down
31 changes: 29 additions & 2 deletions infra/relay/src/environments/ManagedEndpointProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,25 @@ export function isManagedEndpointNotFound(cause: unknown): boolean {
return "cause" in cause && isManagedEndpointNotFound(cause.cause);
}

/**
* Cloudflare refuses to delete a tunnel while a connector is still attached,
* either one that has not finished draining or another runtime still serving
* the tunnel.
*/
export function isManagedEndpointTunnelInUse(cause: unknown): boolean {
if (typeof cause !== "object" || cause === null) {
return false;
}
if (
"message" in cause &&
typeof cause.message === "string" &&
cause.message.includes("has active connections")
) {
return true;
}
return "cause" in cause && isManagedEndpointTunnelInUse(cause.cause);
}

type ManagedEndpointClientError = ManagedEndpointTunnelClientError | ManagedEndpointDnsClientError;

const ignoreNotFound = <A>(
Expand Down Expand Up @@ -873,8 +892,16 @@ export const make = Effect.gen(function* () {
if (finalGeneration === null) {
return false;
}
yield* deleteTunnel;
return true;
// A connector still attached means the tunnel is not released. That
// is the same answer as losing the claim: the caller keeps its config,
// and the reaper deletes the tunnel once it has been down long enough.
return yield* deleteTunnel.pipe(
Effect.as(true),
Effect.catchIf(
(error) => isManagedEndpointTunnelInUse(error.cause),
() => Effect.succeed(false),
),
);
}),
)
.pipe(
Expand Down
Loading