Skip to content

Commit

Permalink
feat(user): implement dry run and summary logging for stale anonymous…
Browse files Browse the repository at this point in the history
… users cleaner

refs #464
  • Loading branch information
ygrishajev committed Nov 15, 2024
1 parent b258c63 commit fa1ae4d
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { InjectWallet } from "@src/billing/providers/wallet.provider";
import { MasterSigningClientService } from "@src/billing/services/master-signing-client/master-signing-client.service";
import { MasterWalletService } from "@src/billing/services/master-wallet/master-wallet.service";
import { RpcMessageService, SpendingAuthorizationMsgOptions } from "@src/billing/services/rpc-message-service/rpc-message.service";
import { DryRunOptions } from "@src/core/types/console";

interface SpendingAuthorizationOptions {
address: string;
Expand Down Expand Up @@ -110,25 +111,35 @@ export class ManagedUserWalletService {
return await this.masterSigningClientService.executeTx([deploymentAllowanceMsg]);
}

async revokeAll(grantee: string, reason?: string) {
async revokeAll(grantee: string, reason?: string, options?: DryRunOptions) {
const masterWalletAddress = await this.masterWalletService.getFirstAddress();
const params = { granter: masterWalletAddress, grantee };
const messages: EncodeObject[] = [];
const revokeTypes: string[] = [];
const revokeSummary = {
feeAllowance: false,
deploymentGrant: false
};

if (await this.allowanceHttpService.hasFeeAllowance(params.granter, params.grantee)) {
revokeTypes.push("REVOKE_ALLOWANCE");
revokeSummary.feeAllowance = true;
messages.push(this.rpcMessageService.getRevokeAllowanceMsg(params));
}

if (await this.allowanceHttpService.hasDeploymentGrant(params.granter, params.grantee)) {
revokeTypes.push("REVOKE_DEPOSIT_DEPLOYMENT_GRANT");
revokeSummary.deploymentGrant = true;
messages.push(this.rpcMessageService.getRevokeDepositDeploymentGrantMsg(params));
}

if (messages.length) {
if (!messages.length) {
return;
}

if (!options?.dryRun) {
await this.masterSigningClientService.executeTx(messages);
this.logger.info({ event: "SPENDING_REVOKED", address: params.grantee, revokeTypes, reason });
}

this.logger.info({ event: "SPENDING_REVOKED", address: params.grantee, revokeSummary, reason });

return revokeSummary;
}
}
5 changes: 3 additions & 2 deletions apps/api/src/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ program

program
.command("top-up-deployments")
.option("-d, --dry-run", "Dry run the top up deployments", false)
.description("Refill deployments with auto top up enabled")
.option("-d, --dry-run", "Dry run the top up deployments", false)
.action(async (options, command) => {
await executeCliHandler(command.name(), async () => {
await container.resolve(TopUpDeploymentsController).topUpDeployments({ dryRun: options.dryRun });
Expand All @@ -51,9 +51,10 @@ const userConfig = container.resolve(UserConfigService);
program
.command("cleanup-stale-anonymous-users")
.description(`Remove users that have been inactive for ${userConfig.get("STALE_ANONYMOUS_USERS_LIVE_IN_DAYS")} days`)
.option("-d, --dry-run", "Dry run the clean up stale anonymous users", false)
.action(async (options, command) => {
await executeCliHandler(command.name(), async () => {
await container.resolve(UserController).cleanUpStaleAnonymousUsers();
await container.resolve(UserController).cleanUpStaleAnonymousUsers({ dryRun: options.dryRun });
});
});

Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/core/types/console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface DryRunOptions {
dryRun: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class TopUpManagedDeploymentsService implements DeploymentsRefiller {
});

summary.set("endBlockHeight", await this.blockHttpService.getCurrentHeight());
this.logger.info({ event: "TOP_UP_SUMMARY", summary: summary.summarize() });
this.logger.info({ event: "TOP_UP_SUMMARY", summary: summary.summarize(), dryRun: options.dryRun });
}

private async topUpForWallet(wallet: UserWalletOutput, options: TopUpDeploymentsOptions, summary: TopUpSummarizer) {
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/deployment/types/deployments-refiller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface TopUpDeploymentsOptions {
dryRun: boolean;
}
import { DryRunOptions } from "@src/core/types/console";

export interface TopUpDeploymentsOptions extends DryRunOptions {}

export interface DeploymentsRefiller {
topUpDeployments(options: TopUpDeploymentsOptions): Promise<void>;
Expand Down
9 changes: 6 additions & 3 deletions apps/api/src/user/controllers/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { AuthTokenService } from "@src/auth/services/auth-token/auth-token.servi
import { UserRepository } from "@src/user/repositories";
import { GetUserParams } from "@src/user/routes/get-anonymous-user/get-anonymous-user.router";
import { AnonymousUserResponseOutput } from "@src/user/schemas/user.schema";
import { StaleAnonymousUsersCleanerService } from "@src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service";
import {
StaleAnonymousUsersCleanerOptions,
StaleAnonymousUsersCleanerService
} from "@src/user/services/stale-anonymous-users-cleaner/stale-anonymous-users-cleaner.service";

@singleton()
export class UserController {
Expand Down Expand Up @@ -34,7 +37,7 @@ export class UserController {
return { data: user };
}

async cleanUpStaleAnonymousUsers() {
await this.staleAnonymousUsersCleanerService.cleanUpStaleAnonymousUsers();
async cleanUpStaleAnonymousUsers(options: StaleAnonymousUsersCleanerOptions) {
await this.staleAnonymousUsersCleanerService.cleanUpStaleAnonymousUsers(options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
interface StaleAnonymousUsersCleanerSummary {
feeAllowanceRevokeCount: number;
deploymentGrantRevokeCount: number;
revokeErrorCount: number;
usersDroppedCount: number;
}

export class StaleAnonymousUsersCleanerSummarizer {
private feeAllowanceRevokeCount = 0;

private deploymentGrantRevokeCount = 0;

private revokeErrorCount = 0;

private usersDroppedCount = 0;

inc(param: keyof StaleAnonymousUsersCleanerSummary, value = 1) {
this[param] += value;
}

summarize(): StaleAnonymousUsersCleanerSummary {
return {
feeAllowanceRevokeCount: this.feeAllowanceRevokeCount,
deploymentGrantRevokeCount: this.deploymentGrantRevokeCount,
revokeErrorCount: this.revokeErrorCount,
usersDroppedCount: this.usersDroppedCount
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import { UserWalletRepository } from "@src/billing/repositories";
import { ManagedUserWalletService } from "@src/billing/services";
import { InjectSentry, Sentry } from "@src/core/providers/sentry.provider";
import { SentryEventService } from "@src/core/services/sentry-event/sentry-event.service";
import { DryRunOptions } from "@src/core/types/console";
import { UserRepository } from "@src/user/repositories";
import { StaleAnonymousUsersCleanerSummarizer } from "@src/user/services/stale-anonymous-users-cleaner-summarizer/stale-anonymous-users-cleaner-summarizer.service";
import { UserConfigService } from "@src/user/services/user-config/user-config.service";

export interface StaleAnonymousUsersCleanerOptions extends DryRunOptions {}

@singleton()
export class StaleAnonymousUsersCleanerService {
private readonly CONCURRENCY = 10;
Expand All @@ -25,7 +29,8 @@ export class StaleAnonymousUsersCleanerService {
private readonly sentryEventService: SentryEventService
) {}

async cleanUpStaleAnonymousUsers() {
async cleanUpStaleAnonymousUsers(options: StaleAnonymousUsersCleanerOptions) {
const summary = new StaleAnonymousUsersCleanerSummarizer();
await this.userRepository.paginateStaleAnonymousUsers(
{ inactivityInDays: this.config.get("STALE_ANONYMOUS_USERS_LIVE_IN_DAYS"), limit: this.CONCURRENCY },
async users => {
Expand All @@ -34,20 +39,30 @@ export class StaleAnonymousUsersCleanerService {
const { errors } = await PromisePool.withConcurrency(this.CONCURRENCY)
.for(wallets)
.process(async wallet => {
await this.managedUserWalletService.revokeAll(wallet.address, "USER_INACTIVITY");
const result = await this.managedUserWalletService.revokeAll(wallet.address, "USER_INACTIVITY", options);
if (result.feeAllowance) {
summary.inc("feeAllowanceRevokeCount");
}
if (result.deploymentGrant) {
summary.inc("deploymentGrantRevokeCount");
}
});
const erroredUserIds = errors.map(({ item }) => item.userId);
const userIdsToRemove = difference(userIds, erroredUserIds);

if (userIdsToRemove.length) {
await this.userRepository.deleteById(userIdsToRemove);
this.logger.debug({ event: "STALE_ANONYMOUS_USERS_CLEANUP", userIds: userIdsToRemove });
}

if (errors.length) {
summary.inc("revokeErrorCount", errors.length);
this.logger.debug({ event: "STALE_ANONYMOUS_USERS_REVOKE_ERROR", errors });
this.sentry.captureEvent(this.sentryEventService.toEvent(errors));
}

if (userIdsToRemove.length) {
if (!options.dryRun) {
await this.userRepository.deleteById(userIdsToRemove);
}
summary.inc("usersDroppedCount", userIdsToRemove.length);
}
this.logger.debug({ event: "STALE_ANONYMOUS_USERS_CLEANUP", userIds: userIdsToRemove, summary: summary.summarize(), dryRun: options.dryRun });
}
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe("Users", () => {
const reactivate = walletService.getWalletByUserId(reactivated.user.id, reactivated.token);
await reactivate;

await controller.cleanUpStaleAnonymousUsers();
await controller.cleanUpStaleAnonymousUsers({ dryRun: false });

const [users, wallets] = await Promise.all([userRepository.find(), userWalletRepository.find()]);

Expand Down

0 comments on commit fa1ae4d

Please sign in to comment.