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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
createDeployment,
createDeploymentGroup,
createDeploymentGroupResource,
createLease,
createProvider,
createTransaction
} from "@test/seeders";

Expand Down Expand Up @@ -179,6 +181,76 @@ describe(DeploymentRepository.name, () => {
});
});

describe("findStaleDeployments", () => {
it("returns an open deployment that never had a lease", async () => {
const { repository, owner, base } = setup();
const deployment = await seedOpenDeployment(owner, { createdHeight: base - 1 });

const found = await repository.findStaleDeployments({ owner, staleBeforeHeight: base });

expect(found.map(stale => String(stale.dseq))).toEqual([deployment.dseq]);
});

it("returns an open deployment whose every lease closed before the cutoff", async () => {
const { repository, owner, base } = setup();
const deployment = await seedOpenDeployment(owner, { createdHeight: base - 1_000 });
await seedLease(deployment, { closedHeight: base - 1 });

const found = await repository.findStaleDeployments({ owner, staleBeforeHeight: base });

expect(found.map(stale => String(stale.dseq))).toEqual([deployment.dseq]);
});

it("leaves out a deployment whose last lease closed at or after the cutoff", async () => {
const { repository, owner, base } = setup();
const deployment = await seedOpenDeployment(owner, { createdHeight: base - 1_000 });
await seedLease(deployment, { gseq: 1, closedHeight: base - 500 });
await seedLease(deployment, { gseq: 2, closedHeight: base });

const found = await repository.findStaleDeployments({ owner, staleBeforeHeight: base });

expect(found).toEqual([]);
});

it("leaves out a deployment that still holds an active lease", async () => {
const { repository, owner, base } = setup();
const deployment = await seedOpenDeployment(owner, { createdHeight: base - 1_000 });
await seedLease(deployment, { gseq: 1, closedHeight: base - 500 });
await seedLease(deployment, { gseq: 2 });

const found = await repository.findStaleDeployments({ owner, staleBeforeHeight: base });

expect(found).toEqual([]);
});

it("leaves out a deployment created at or after the cutoff", async () => {
const { repository, owner, base } = setup();
await seedOpenDeployment(owner, { createdHeight: base });

const found = await repository.findStaleDeployments({ owner, staleBeforeHeight: base });

expect(found).toEqual([]);
});

it("leaves out a deployment that is already closed", async () => {
const { repository, owner, base } = setup();
await seedOpenDeployment(owner, { createdHeight: base - 1_000, closedHeight: base - 500 });

const found = await repository.findStaleDeployments({ owner, staleBeforeHeight: base });

expect(found).toEqual([]);
});

it("leaves out the deployments of other owners", async () => {
const { repository, owner, base } = setup();
await seedOpenDeployment(createAkashAddress(), { createdHeight: base - 1 });

const found = await repository.findStaleDeployments({ owner, staleBeforeHeight: base });

expect(found).toEqual([]);
});
});

describe("countActiveByOwner", () => {
const WINDOW = { startDate: "2025-03-01", endDate: "2025-03-31" };

Expand Down Expand Up @@ -296,6 +368,29 @@ describe(DeploymentRepository.name, () => {
return height;
}

async function seedOpenDeployment(owner: string, input: { createdHeight: number; closedHeight?: number }) {
const deployment = await createDeployment({ owner, dseq: faker.string.numeric(12), ...input });

return { id: deployment.id, owner: deployment.owner, dseq: deployment.dseq };
}

async function seedLease(deployment: { id: string; owner: string; dseq: string }, input: { gseq?: number; closedHeight?: number }) {
const gseq = input.gseq ?? 1;
const provider = await createProvider();
const group = await createDeploymentGroup({ deploymentId: deployment.id, owner: deployment.owner, dseq: deployment.dseq, gseq });

return await createLease({
deploymentId: deployment.id,
deploymentGroupId: group.id,
owner: deployment.owner,
dseq: deployment.dseq,
gseq,
oseq: 1,
providerAddress: provider.owner,
closedHeight: input.closedHeight
});
}

async function seedGpuDeployment(input: {
createdHeight: number;
bidHeight: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { inject, singleton } from "tsyringe";
import { CHAIN_DB } from "@src/chain";

export interface StaleDeploymentsOptions {
createdHeight: number;
staleBeforeHeight: number;
owner: string;
}

Expand All @@ -27,6 +27,14 @@ export interface DeploymentActivityWindow {
endDate: string;
}

/** Heights reach the query through a literal, so anything but a plain integer is refused before it can be interpolated. */
function asHeight(value: number): number {
if (!Number.isSafeInteger(value)) {
throw new TypeError(`Expected a block height, received ${value}`);
}
return value;
}

function startOfDayAfter(date: string) {
const dayAfter = new Date(`${date}T00:00:00.000Z`);
dayAfter.setUTCDate(dayAfter.getUTCDate() + 1);
Expand Down Expand Up @@ -118,12 +126,15 @@ export class DeploymentRepository {
where: {
owner: options.owner,
createdHeight: {
[Op.lt]: options.createdHeight
[Op.lt]: options.staleBeforeHeight
},
closedHeight: null
},
group: ["deployment.dseq"],
having: literal(`COUNT("leases"."deploymentId") = 0`),
having: literal(
`COUNT("leases"."deploymentId") FILTER (WHERE "leases"."closedHeight" IS NULL) = 0 ` +
`AND COALESCE(MAX("leases"."closedHeight"), 0) < ${asHeight(options.staleBeforeHeight)}`
),
raw: true
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe(StaleManagedDeploymentsCleanerService.name, () => {

await service.cleanUpForWallet(wallet);

const cutoff = deploymentRepository.findStaleDeployments.mock.calls[0][0].createdHeight;
const cutoff = deploymentRepository.findStaleDeployments.mock.calls[0][0].staleBeforeHeight;
expect(cutoff).toBeLessThan(1_000_000);
});

Expand All @@ -42,7 +42,7 @@ describe(StaleManagedDeploymentsCleanerService.name, () => {

await service.cleanUpForWallet(wallet, 0);

expect(deploymentRepository.findStaleDeployments).toHaveBeenCalledWith({ owner: wallet.address, createdHeight: 1_000_000 });
expect(deploymentRepository.findStaleDeployments).toHaveBeenCalledWith({ owner: wallet.address, staleBeforeHeight: 1_000_000 });
});

it("reads the chain height itself when called for a single wallet", async () => {
Expand Down Expand Up @@ -255,7 +255,7 @@ describe(StaleManagedDeploymentsCleanerService.name, () => {

await service.cleanup({ concurrency: 2 });

const cutoffs = new Set(deploymentRepository.findStaleDeployments.mock.calls.map(([{ createdHeight }]) => createdHeight));
const cutoffs = new Set(deploymentRepository.findStaleDeployments.mock.calls.map(([{ staleBeforeHeight }]) => staleBeforeHeight));
expect(cutoffs.size).toBe(1);
expect([...cutoffs][0]).toBeLessThan(1_000_000);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class StaleManagedDeploymentsCleanerService {
event: "DEPLOYMENT_CLEAN_UP_ERROR",
context: StaleManagedDeploymentsCleanerService.name
},
() => this.#closeLeaselessDeployments(wallet, staleBeforeHeight)
() => this.#closeDeploymentsWithoutActiveLease(wallet, staleBeforeHeight)
);
});

Expand All @@ -58,7 +58,7 @@ export class StaleManagedDeploymentsCleanerService {
}

async cleanUpForWallet(wallet: UserWalletOutput, maxLiveBlocks: number = this.MAX_LIVE_BLOCKS) {
await this.#closeLeaselessDeployments(wallet, await this.#resolveStaleBeforeHeight(maxLiveBlocks));
await this.#closeDeploymentsWithoutActiveLease(wallet, await this.#resolveStaleBeforeHeight(maxLiveBlocks));
}

/** Read once per sweep instead of per wallet: the tip is the same for every one of them, and the sweep walks the whole managed-wallet table. */
Expand All @@ -67,10 +67,10 @@ export class StaleManagedDeploymentsCleanerService {
}

/** Dropping a message and re-broadcasting is safe because both classified failures reject the tx whole: an estimate never lands, a non-zero code reverts. */
async #closeLeaselessDeployments(wallet: UserWalletOutput, staleBeforeHeight: number) {
async #closeDeploymentsWithoutActiveLease(wallet: UserWalletOutput, staleBeforeHeight: number) {
let remaining = await this.deploymentRepository.findStaleDeployments({
owner: wallet.address!,
createdHeight: staleBeforeHeight
staleBeforeHeight
});

if (!remaining.length) {
Expand Down