Skip to content

feat: fix #814 stats routes, #824 webhook deliveries, #826 redis scan… - #855

Open
peacebas wants to merge 2 commits into
AudioBitsStellar:mainfrom
peacebas:feat/814-824-826-834-stats-webhooks-scan-handles
Open

peacebas wants to merge 2 commits into
AudioBitsStellar:mainfrom
peacebas:feat/814-824-826-834-stats-webhooks-scan-handles

Conversation

@peacebas

Copy link
Copy Markdown

Closes #814
Closes #824
Closes #826
Closes #834

1. Route Song & Artist Stats Endpoints

  • Problem: SongStatsService provided comprehensive aggregation queries powering SongController.getSongStats and SongController.getArtistStats, but neither handler was routed. Artist dashboard analytics had no endpoints to query.
  • Solution & Flow:
    • Mounted GET /api/song/:id/stats with optional authentication.
    • Mounted GET /api/song/artist/:artistId/stats and GET /api/artist/:id/stats for artist catalog analytics.
    • Enforced metric privacy: Public/unauthenticated callers and non-owners receive only public engagement metrics (plays and saves). Confidential financial metrics (revenueStroops, denominated in atomic Stellar stroops representing $10^{-7}$ XLM) and audience demographics (uniqueListeners) are strictly restricted to the verified song/artist owner, platform admins, and moderators.
    • Added time-window validation (24h, 7d, 30d, 90d, all-time) returning structured 400 Bad Request (INVALID_WINDOW) errors for unsupported values.
    • Added HTTP cache headers: Cache-Control: private, max-age=300 for creators and Cache-Control: public, max-age=300 for public metrics, combined with Vary: Authorization to prevent cache poisoning across intermediate proxies.
    • Documented OpenAPI schemas and added integration tests.

2. Record Webhook Delivery Attempts & Expose Redelivery Endpoint

  • Problem: Failed webhook deliveries only left a log line. Integrators had no ability to inspect request payloads, response codes, or latency, or trigger redeliveries after fixing broken endpoints. Subscriptions that failed forever also continued receiving (and failing) events indefinitely.
  • Solution & Flow:
    • Introduced the WebhookDelivery entity recording subscriptionId, eventId, eventType, attempt, status (pending|succeeded|failed), responseStatus, durationMs, error, and payload with database indexes on subscriptionId, createdAt, and eventId.
    • Added consecutiveFailures counter to WebhookSubscription and created database migration 1757700000000-AddWebhookDeliveriesAndFailureTracking.ts.
    • In WebhookService.deliver(), each HTTP attempt writes an audit row. Successful deliveries reset consecutiveFailures = 0.
    • Auto-disable circuit breaker: When all retry attempts for an event fail, consecutiveFailures increments. If consecutive failures reach the configurable threshold (WEBHOOK_MAX_CONSECUTIVE_FAILURES, default 5), the subscription is automatically disabled (isActive = false) and an alert notification is dispatched to the owner via NotificationService.
    • Mounted owner-only endpoints:
      • GET /api/webhooks/:id/deliveries (paginated, newest first).
      • POST /api/webhooks/:id/deliveries/:deliveryId/redeliver (triggers retry using original signed payload).
    • Implemented 30-day retention cleanup routine (cleanupOldDeliveries(30)) and standalone job src/jobs/WebhookRetentionJob.ts.
    • Added OpenAPI specs and test coverage.

3. Use SCAN Instead of KEYS in CacheService.invalidatePattern

  • Problem: CacheService.invalidatePattern executed redis.keys(pattern). KEYS is a blocking $\mathcal{O}(N)$ command over the entire key space that freezes the single-threaded Redis event loop, stalling rate limiters, session lookups, and token verifications. Additionally, AccountDeletionService invoked CacheService.invalidatePattern('song:*'), scanning and dropping all song cache keys across the entire platform whenever any single user deleted their account.
  • Solution & Flow:
    • Reimplemented CacheService.invalidatePattern using non-blocking cursor iteration via redis.scanStream({ match: pattern, count: 500 }).
    • Batched matching keys and deleted them via redis.unlink(...keys) in chunks of 500. UNLINK removes keys from the keyspace in $\mathcal{O}(1)$ time and lazily deallocates memory in background threads without blocking concurrent commands.
    • Refactored AccountDeletionService to selectively query the deleting artist's songs (await songRepo.find({ where: { artistId: user.id }, select: ['id'] })) and surgically clear only their keys via CacheService.clearSong(song.id).
    • Verified 0 occurrences of redis.keys( remain in src/. Added unit tests with mocked scan streams.

4. Run Jest with --detectOpenHandles in CI & Fix Leaking Timers

  • Problem: Unref'd setInterval and setTimeout timers initialized at module import or constructor time in PrivySessionService, DatabaseConnectionManager, DbPoolMonitor, and JobQueueWorker, as well as background Redis socket reconnect timers, kept Node.js event loops alive. Jest test suites could not exit cleanly, leading to hanging workers or masking leaks via --forceExit.
  • Solution & Flow:
    • Removed top-level and constructor-time timers from PrivySessionService; introduced explicit start(), stop(), and .unref() handling.
    • Added stopConnectionStateLogger() and made circuit breaker health check intervals opt-in (autoStart = false) in DatabaseConnectionManager.
    • Configured test-mode Redis options in src/config/redis.ts (lazyConnect: true, retryStrategy: () => null) to prevent background socket connection loops during tests.
    • Added global Jest teardown in tests/setup/jestTeardown.ts to stop remaining background timers and disconnect Redis sockets cleanly.
    • Updated CI workflow .github/workflows/ci.yml to execute npm test -- --detectOpenHandles without --forceExit.

Changed

@drips-wave

drips-wave Bot commented Oct 18, 2026

Copy link
Copy Markdown

@peacebas Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant