Skip to content

Implement account expiry scheduler and INITIALIZING cleanup job #100

@phertyameen

Description

@phertyameen

Summary

Two categories of accounts will accumulate in the database indefinitely without scheduled cleanup: accounts that have passed their expiresAt without being claimed, and accounts stuck in INITIALIZING status due to transient failures during creation. This issue implements both cleanup jobs, the expiry scheduler that calls StellarService.expireAccount() for unclaimed accounts, and a simpler cleanup job that marks stale INITIALIZING accounts as FAILED.

Background: Why both jobs are needed

Expiry scheduler
When an account's expiresAt passes without a claim, the account should transition to EXPIRED status and the on-chain contract should be called via StellarService.expireAccount() to direct the recovery of funds back to the recovery_address. Without this scheduler, accounts stay in PENDING_PAYMENT or PENDING_CLAIM forever after their window closes. The contract's expire() function can only be called after the expiry_ledger is reached, so the scheduler must also verify current ledger state before attempting the call, which expireAccount() already handles internally.
INITIALIZING cleanup

Issue #85 introduced INITIALIZING as a transitional status between DB record creation and successful contract initialization. In the normal case this window is a few seconds. But if the Soroban RPC times out, if the network is unavailable, or if the process crashes mid-creation, the account will be stuck in INITIALIZING indefinitely. These accounts will never become usable and should be marked FAILED after a reasonable timeout, 10 minutes is a sensible default for MVP.

How the expiry scheduler should work

The scheduler runs on a fixed interval, every 5 minutes is reasonable for MVP. On each tick it queries for accounts where status is PENDING_PAYMENT or PENDING_CLAIM and expiresAt is in the past. For each such account it calls StellarService.expireAccount(), which internally checks the current ledger before submitting the contract call. On success it updates account status to EXPIRED and sets expiredAt to the current timestamp (resolving Issue #84). Failures on individual accounts should be logged and not abort the rest of the batch.

How the INITIALIZING cleanup should work

The cleanup job can run less frequently, every 15 minutes is fine. It queries for accounts where status is INITIALIZING and createdAt is older than the configured timeout (default 10 minutes, configurable via INITIALIZING_TIMEOUT_MS). For each such account it sets status to FAILED and stores a reason in metadata, something like { failureReason: 'initialization_timeout', detectedAt: }. No contract call is needed since the contract was never successfully initialized for these accounts.

Shared infrastructure

Both jobs can live in the same module, src/modules/scheduler/ is a natural home. They both need access to the Account repository. The expiry job additionally needs StellarService. Both should use @nestjs/schedule consistent with Issue #98's payment monitor.

Failure handling

Neither job should crash the application on error. Individual account failures should be logged with the account ID and error, and the job should continue with the next account. If the entire job tick fails (e.g. database unavailable), log the error and wait for the next scheduled run.

Environment configuration

Add to .env.example:
EXPIRY_CHECK_INTERVAL_MS=300000 # 5 minutes
INITIALIZING_TIMEOUT_MS=600000 # 10 minutes
INITIALIZING_CLEANUP_INTERVAL_MS=900000 # 15 minutes
Where to look

Acceptance Criteria

  • A SchedulerModule exists in src/modules/scheduler/
  • Expiry job runs on a configurable interval and processes all expired PENDING_PAYMENT and PENDING_CLAIM accounts
  • On successful expiry, account status is set to EXPIRED and expiredAt is set to current timestamp
  • INITIALIZING cleanup job marks accounts older than the configured timeout as FAILED with a reason in metadata
  • Neither job crashes the application on individual account failure
  • All three interval values are configurable via environment variables and documented in .env.example
  • Unit tests cover: expired account processed correctly, non-expired account skipped, stale INITIALIZING account cleaned up, recent INITIALIZING account left alone, single account failure does not abort the batch

Metadata

Metadata

Assignees

No one assigned

    Labels

    criticalVery important for the project to function.enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions