Skip to content

Commit 70c2193

Browse files
authored
feat(deployment): clean up trial deployments for a provider
(#502)
1 parent 4a23e1f commit 70c2193

8 files changed

Lines changed: 177 additions & 100 deletions

File tree

apps/api/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@
5454
"@hono/swagger-ui": "0.2.1",
5555
"@hono/zod-openapi": "0.9.5",
5656
"@octokit/rest": "^18.12.0",
57-
"@opentelemetry/instrumentation": "^0.54.0",
58-
"@opentelemetry/instrumentation-http": "^0.54.0",
59-
"@opentelemetry/sdk-node": "^0.54.0",
57+
"@opentelemetry/instrumentation": "^0.54.2",
58+
"@opentelemetry/instrumentation-http": "^0.54.2",
59+
"@opentelemetry/sdk-node": "^0.54.2",
6060
"@sentry/node": "^7.55.2",
6161
"@supercharge/promise-pool": "^3.2.0",
6262
"@ucast/core": "^1.10.2",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { LoggerService } from "@akashnetwork/logging";
2+
import { singleton } from "tsyringe";
3+
4+
import { BillingConfig, InjectBillingConfig } from "@src/billing/providers";
5+
import { UserWalletOutput, UserWalletRepository } from "@src/billing/repositories";
6+
import { ManagedUserWalletService, RpcMessageService } from "@src/billing/services";
7+
import { ErrorService } from "@src/core/services/error/error.service";
8+
import { ProviderCleanupSummarizer } from "@src/deployment/lib/provider-cleanup-summarizer/provider-cleanup-summarizer";
9+
import { DeploymentRepository } from "@src/deployment/repositories/deployment/deployment.repository";
10+
import { TxSignerService } from "../tx-signer/tx-signer.service";
11+
12+
export interface ProviderCleanupParams {
13+
concurrency: number;
14+
providerAddress: string;
15+
dryRun: boolean;
16+
}
17+
18+
@singleton()
19+
export class ProviderCleanupService {
20+
private readonly logger = LoggerService.forContext(ProviderCleanupService.name);
21+
22+
constructor(
23+
@InjectBillingConfig() private readonly config: BillingConfig,
24+
private readonly userWalletRepository: UserWalletRepository,
25+
private readonly managedUserWalletService: ManagedUserWalletService,
26+
private readonly txSignerService: TxSignerService,
27+
private readonly deploymentRepository: DeploymentRepository,
28+
private readonly rpcMessageService: RpcMessageService,
29+
private readonly errorService: ErrorService
30+
) {}
31+
32+
async cleanup(options: ProviderCleanupParams) {
33+
const summary = new ProviderCleanupSummarizer();
34+
await this.userWalletRepository.paginate({ query: { isTrialing: true }, limit: options.concurrency || 10 }, async wallets => {
35+
const cleanUpAllWallets = wallets.map(async wallet => {
36+
await this.errorService.execWithErrorHandler(
37+
{
38+
wallet,
39+
event: "PROVIDER_CLEAN_UP_ERROR",
40+
context: ProviderCleanupService.name
41+
},
42+
() => this.cleanUpForWallet(wallet, options, summary)
43+
);
44+
});
45+
46+
await Promise.all(cleanUpAllWallets);
47+
});
48+
49+
this.logger.info({ event: "PROVIDER_CLEAN_UP_SUMMARY", summary: summary.summarize(), dryRun: options.dryRun });
50+
}
51+
52+
private async cleanUpForWallet(wallet: UserWalletOutput, options: ProviderCleanupParams, summary: ProviderCleanupSummarizer) {
53+
const client = await this.txSignerService.getClientForAddressIndex(wallet.id);
54+
const deployments = await this.deploymentRepository.findDeploymentsForProvider({
55+
owner: wallet.address,
56+
provider: options.providerAddress
57+
});
58+
59+
const closeAllWalletStaleDeployments = deployments.map(async deployment => {
60+
const message = this.rpcMessageService.getCloseDeploymentMsg(wallet.address, deployment.dseq);
61+
this.logger.info({ event: "PROVIDER_CLEAN_UP", params: { owner: wallet.address, dseq: deployment.dseq } });
62+
63+
try {
64+
if (!options.dryRun) {
65+
await client.signAndBroadcast([message]);
66+
this.logger.info({ event: "PROVIDER_CLEAN_UP_SUCCESS" });
67+
}
68+
} catch (error) {
69+
if (error.message.includes("not allowed to pay fees")) {
70+
if (!options.dryRun) {
71+
await this.managedUserWalletService.authorizeSpending({
72+
address: wallet.address,
73+
limits: {
74+
fees: this.config.FEE_ALLOWANCE_REFILL_AMOUNT
75+
}
76+
});
77+
await client.signAndBroadcast([message]);
78+
this.logger.info({ event: "PROVIDER_CLEAN_UP_SUCCESS" });
79+
}
80+
} else {
81+
throw error;
82+
}
83+
} finally {
84+
summary.inc("deploymentCount");
85+
}
86+
});
87+
88+
await Promise.all(closeAllWalletStaleDeployments);
89+
}
90+
}

apps/api/src/console.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { chainDb } from "@src/db/dbConnection";
1414
import { TopUpDeploymentsController } from "@src/deployment/controllers/deployment/deployment.controller";
1515
import { UserController } from "@src/user/controllers/user/user.controller";
1616
import { UserConfigService } from "@src/user/services/user-config/user-config.service";
17+
import { ProviderController } from "./deployment/controllers/provider/provider.controller";
1718

1819
const program = new Command();
1920

@@ -42,13 +43,25 @@ program
4243
program
4344
.command("cleanup-stale-deployments")
4445
.description("Close deployments without leases created at least 10min ago")
45-
.option("-c, --concurrency <number>", "How much wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value))
46+
.option("-c, --concurrency <number>", "How many wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value))
4647
.action(async (options, command) => {
4748
await executeCliHandler(command.name(), async () => {
4849
await container.resolve(TopUpDeploymentsController).cleanUpStaleDeployment(options);
4950
});
5051
});
5152

53+
program
54+
.command("cleanup-provider-deployments")
55+
.description("Close trial deployments for a provider")
56+
.option("-c, --concurrency <number>", "How many wallets is processed concurrently", value => z.number({ coerce: true }).optional().default(10).parse(value))
57+
.option("-d, --dry-run", "Dry run the trial provider cleanup", false)
58+
.option("-p, --provider <string>", "Provider address", value => z.string().parse(value))
59+
.action(async (options, command) => {
60+
await executeCliHandler(command.name(), async () => {
61+
await container.resolve(ProviderController).cleanupProviderDeployments(options);
62+
});
63+
});
64+
5265
const userConfig = container.resolve(UserConfigService);
5366
program
5467
.command("cleanup-stale-anonymous-users")
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { singleton } from "tsyringe";
22

3+
import { ProviderCleanupParams, ProviderCleanupService } from "@src/billing/services/provider-cleanup/provider-cleanup.service";
34
import { TrialProvidersService } from "@src/deployment/services/trial-providers/trial-providers.service";
45

56
@singleton()
67
export class ProviderController {
7-
constructor(private readonly trialProvidersService: TrialProvidersService) {}
8+
constructor(
9+
private readonly trialProvidersService: TrialProvidersService,
10+
private readonly providerCleanupService: ProviderCleanupService
11+
) {}
812

913
async getTrialProviders(): Promise<string[]> {
1014
return await this.trialProvidersService.getTrialProviders();
1115
}
16+
17+
async cleanupProviderDeployments(options: ProviderCleanupParams) {
18+
return await this.providerCleanupService.cleanup(options);
19+
}
1220
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
interface ProviderCleanupSummary {
2+
deploymentCount: number;
3+
}
4+
5+
export class ProviderCleanupSummarizer {
6+
private deploymentCount = 0;
7+
8+
inc(param: keyof ProviderCleanupSummary, value = 1) {
9+
this[param] += value;
10+
}
11+
12+
set(param: keyof ProviderCleanupSummary, value: number) {
13+
this[param] = value;
14+
}
15+
16+
get(param: keyof ProviderCleanupSummary) {
17+
return this[param];
18+
}
19+
20+
summarize(): ProviderCleanupSummary {
21+
return {
22+
deploymentCount: this.deploymentCount
23+
};
24+
}
25+
}

apps/api/src/deployment/repositories/deployment/deployment.repository.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ export interface StaleDeploymentsOptions {
77
owner: string;
88
}
99

10+
export interface ProviderCleanupOptions {
11+
owner: string;
12+
provider: string;
13+
}
14+
1015
export interface StaleDeploymentsOutput {
1116
dseq: number;
1217
}
@@ -37,4 +42,27 @@ export class DeploymentRepository {
3742

3843
return deployments ? (deployments as unknown as StaleDeploymentsOutput[]) : [];
3944
}
45+
46+
async findDeploymentsForProvider(options: ProviderCleanupOptions): Promise<StaleDeploymentsOutput[]> {
47+
const deployments = await Deployment.findAll({
48+
attributes: ["dseq"],
49+
where: {
50+
owner: options.owner,
51+
closedHeight: null
52+
},
53+
include: [
54+
{
55+
model: Lease,
56+
attributes: [],
57+
required: true,
58+
where: {
59+
provider: options.provider
60+
}
61+
}
62+
],
63+
raw: true
64+
});
65+
66+
return deployments ? (deployments as unknown as StaleDeploymentsOutput[]) : [];
67+
}
4068
}

apps/provider-console/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
"@mui/icons-material": "^5.11.11",
2323
"@mui/material": "^5.4.4",
2424
"@mui/material-nextjs": "^5.15.11",
25-
"@opentelemetry/instrumentation-lru-memoizer": "^0.41.0",
2625
"@radix-ui/react-icons": "^1.3.0",
2726
"@sentry/nextjs": "^8.34.0",
2827
"@sentry/tracing": "^7.114.0",

package-lock.json

Lines changed: 8 additions & 94 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)