Skip to content

[webhooks] webhooks queue#297

Merged
capcom6 merged 1 commit intomasterfrom
issue/286-optimize-webhooks-processing
Dec 5, 2025
Merged

[webhooks] webhooks queue#297
capcom6 merged 1 commit intomasterfrom
issue/286-optimize-webhooks-processing

Conversation

@capcom6
Copy link
Copy Markdown
Owner

@capcom6 capcom6 commented Dec 2, 2025

Summary by CodeRabbit

  • New Features

    • Persistent webhook queue with enqueue API, background processor (start/stop), foreground notification, batched sending, signing, and detailed per-webhook + event logging.
    • Robust retry/backoff, permanent-failure handling, recovery of stuck items, queue statistics, and an enqueue fallback for reliability.
    • Webhooks emission reworked to queue each webhook payload.
  • Chores

    • Queue DAO/repository exposed via DI; added localized string "Processing webhook queue…" (zh + ru formatting).

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 2, 2025

Walkthrough

Adds a persistent webhook queue: Room DB schema v17 snapshot, new webhook_queue entity, DAO, converters and status enum, repository with retry/backoff and recovery, enqueue worker and a coroutine processor worker, DI bindings, WebHooksService integration (logging + queuing), and localized string additions.

Changes

Cohort / File(s) Summary
Database snapshot & DB config
app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json, app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt
Add DB schema v17 snapshot; bump Room DB version 16→17 with AutoMigration; include WebhookQueueEntity and WebhookConverters; expose webhookQueueDao()
Queue entity & schema
app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueEntity.kt
New WebhookQueueEntity mapping webhook_queue table with id, url, payload, retryCount, status, createdAt, nextAttempt, lastError and indices on (status,next_attempt) and (status,created_at)
DAO & statistics
app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueDao.kt
New WebhookQueueDao with insert/query/update methods, recovery, cleanup, and WebhookQueueStatistics
Repository & helpers
app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt
New WebhookQueueRepository encapsulating enqueue, pending retrieval, processing transitions, exponential backoff retry scheduling, permanent failure, cleanup, and recovery; adds canRetry/isProcessing extensions
Status enum & converters
app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookStatus.kt, app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookConverters.kt
WebhookStatus enum with helpers and WebhookConverters Room TypeConverters for serializing status
Workers & processing
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt, app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt
SendWebhookWorker now enqueues payloads to persistent queue with TemporaryStorage/Work fallback and triggers processor; new WebhookQueueProcessorWorker CoroutineWorker processes batches, sends HTTP requests, handles retries/backoff, recovers stuck items, and provides foreground notification
Service integration & logging
app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt, app/src/main/java/me/capcom/smsgateway/modules/webhooks/EventsReceiver.kt, app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt
WebHooksService constructor now accepts LogsService; emit signature changed to emit(context, event, payload) and switched to per-webhook queuing with per-webhook logging; call sites updated to pass Context
Dependency injection
app/src/main/java/me/capcom/smsgateway/data/Module.kt, app/src/main/java/me/capcom/smsgateway/modules/webhooks/Module.kt
Register webhookQueueDao() in DI and add WebhookQueueRepository singleton binding
Resources / localization
app/src/main/res/values/strings.xml, app/src/main/res/values-zh/strings.xml, app/src/main/res/values-ru/strings.xml
Add processing_webhook_queue string (EN & ZH); RU strings reformatted (wrapping only)

Sequence Diagram(s)

sequenceDiagram
    participant WS as WebHooksService
    participant SW as SendWebhookWorker
    participant WQR as WebhookQueueRepository
    participant DB as Room DB
    participant WQP as WebhookQueueProcessorWorker
    participant HTTP as HTTP Client
    participant LOG as LogsService

    WS->>SW: emit(event) per-webhook (with Context)
    SW->>WQR: enqueueWebhook(payload)
    WQR->>DB: INSERT INTO webhook_queue(...)
    DB-->>WQR: id
    SW->>WQP: start/trigger processor

    WQP->>WQR: recoverStuckProcessingWebhooks()
    WQP->>WQR: getPendingWebhooks(now, limit)
    WQR->>DB: SELECT pending WHERE next_attempt <= now
    DB-->>WQR: list

    loop per webhook
        WQP->>WQR: markAsProcessing(id)
        WQP->>HTTP: POST payload -> url
        alt 2xx
            HTTP-->>WQP: success
            WQP->>WQR: markAsCompleted(id)
            WQP->>LOG: log success
        else non-2xx / error
            HTTP-->>WQP: error
            WQP->>WQR: markAsFailed(id, nextAttempt, error)
            WQP->>LOG: log failure
        end
    end

    WQP->>WQR: cleanupOldEntries(cutoff)
    WQR->>DB: DELETE old rows
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Attention areas:
    • WebhookQueueProcessorWorker: coroutine lifecycle, batching, cancellation, foreground notification, HTTP client signing and timeouts.
    • Retry/backoff semantics (Repository ↔ Worker) and correctness of max-retry/permanent-fail transitions.
    • DAO SQL queries, indices, and Room migration v16→v17 (schema JSON).
    • SendWebhookWorker enqueue path: runBlocking usage and TemporaryStorage + Work fallback.
    • WebHooksService emit signature change and propagation to call sites; LogsService integration and per-webhook logging.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.04% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[webhooks] webhooks queue' accurately summarizes the main change: implementing a persistent webhook processing queue system across the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue/286-optimize-webhooks-processing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (6)
app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json (1)

341-344: Consider adding an index on webhook_id if individual webhook queries are expected.

There's no index on webhook_id alone. If you anticipate queries that filter or group by webhook_id (e.g., getting all pending items for a specific webhook), an additional index could improve performance. However, if such queries are not planned, the current indices are sufficient.

Also applies to: 391-412

app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (1)

153-168: Move get<Context>() outside the loop to avoid redundant DI lookups.

get<Context>() is called on every iteration. Since the context doesn't change, retrieve it once before the loop.

+        val context = get<Context>()
+        
         webhooksToProcess.forEach { webhook ->
             // ... existing skip logic ...
             
             try {
                 // Create the webhook event DTO
                 val webhookEventDTO = WebHookEventDTO(
                     id = NanoIdUtils.randomNanoId(),
                     webhookId = webhook.id,
                     event = event,
                     deviceId = deviceId,
                     payload = payload,
                 )

                 SendWebhookWorker.start(
-                    context = get<Context>(),
+                    context = context,
                     url = webhook.url,
                     data = webhookEventDTO,
                     internetRequired = webhooksSettings.internetRequired
                 )
app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookStatus.kt (1)

24-27: Consider using entries instead of values() for future Kotlin compatibility.

values() works but entries is the recommended approach in modern Kotlin.

     companion object {
         fun fromValue(value: String): WebhookStatus? {
-            return values().find { it.value == value }
+            return entries.find { it.value == value }
         }
app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueEntity.kt (1)

36-40: Consider adding defaultValue to createdAt and nextAttempt for schema consistency.

These columns have Kotlin defaults but lack Room defaultValue annotations. While Kotlin defaults work for normal inserts via the DAO, adding schema defaults ensures consistency if raw SQL or migrations ever insert rows directly.

-    @ColumnInfo(name = "created_at")
+    @ColumnInfo(name = "created_at", defaultValue = "0")
     val createdAt: Long = System.currentTimeMillis(),

-    @ColumnInfo(name = "next_attempt")
+    @ColumnInfo(name = "next_attempt", defaultValue = "0")
     val nextAttempt: Long = System.currentTimeMillis(),

Note: SQLite doesn't support CURRENT_TIMESTAMP for INTEGER columns directly, so 0 is a placeholder; the actual default will still come from Kotlin.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueDao.kt (1)

32-43: Inconsistent string quoting in SQL queries.

The query uses double quotes ("pending", "failed") while other queries like scheduledWebhooksCount use single quotes ('pending', 'failed'). While SQLite accepts both, single quotes are SQL standard for string literals.

     @Query(
         """
         SELECT * FROM webhook_queue 
-        WHERE status IN ("pending", "failed") AND next_attempt <= :currentTime 
+        WHERE status IN ('pending', 'failed') AND next_attempt <= :currentTime 
         ORDER BY next_attempt ASC 
         LIMIT :limit
     """
     )

Apply similar changes to other queries using double quotes (lines 51, 65, 84, 98, 130, 142).

app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt (1)

111-118: Consider adding a maximum cap to prevent excessive backoff delays.

With configurable maxRetries, high retry counts could produce impractical delays (e.g., retryCount=20 → ~30 days). Consider capping the delay:

     private fun calculateBackoffDelay(retryCount: Int, baseDelayMs: Long): Long {
         // Exponential backoff: baseDelay * 2^(retryCount - 1)
-        val multiplier = (1L shl (retryCount - 1))
-        return baseDelayMs * multiplier
+        val multiplier = (1L shl minOf(retryCount - 1, 10)) // Cap at 2^10 = 1024x
+        return minOf(baseDelayMs * multiplier, MAX_BACKOFF_DELAY_MS)
     }
+
+    companion object {
+        private const val MAX_BACKOFF_DELAY_MS = 3600_000L // 1 hour max
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7b3cfe and d4cc7c9.

📒 Files selected for processing (15)
  • app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt (3 hunks)
  • app/src/main/java/me/capcom/smsgateway/data/Module.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/Module.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (4 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookConverters.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueDao.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueEntity.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookStatus.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (4 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (1 hunks)
  • app/src/main/res/values-ru/strings.xml (6 hunks)
  • app/src/main/res/values-zh/strings.xml (1 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/TemporaryStorage.kt (1)
  • get (13-15)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)
  • getForegroundInfo (167-169)
app/src/main/java/me/capcom/smsgateway/extensions/GsonBuilder.kt (1)
  • configure (7-9)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build release / build-apk
  • GitHub Check: Build insecure / build-apk
🔇 Additional comments (26)
app/src/main/res/values-zh/strings.xml (1)

88-88: Localization addition looks good.

The Chinese translation for the webhook queue processing status is consistent with the English and Russian variants. String key matches across all locales.

app/src/main/res/values/strings.xml (1)

85-85: English string addition is appropriate.

The processing_webhook_queue string follows the pattern of similar status messages and integrates well with the existing localization structure.

app/src/main/res/values-ru/strings.xml (2)

94-94: Russian webhook queue string is consistent.

The Russian translation matches the localization pattern across all three language variants. String key is identical to English and Chinese versions.


3-4: Line-wrapping changes are formatting-only.

The long string resources have been reformatted to wrap across multiple lines for improved XML readability. These changes do not alter resource values or behavior—purely stylistic. This is consistent with the AI summary noting no semantic changes.

Also applies to: 9-10, 16-19, 45-46, 66-67, 72-73, 82-83, 98-99, 118-119, 120-121, 122-123, 125-126, 127-128

app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (2)

249-291: Good fallback mechanism for resilience.

The fallback to direct WorkManager processing when queue enqueue fails is a sensible defensive pattern that ensures webhooks are still delivered even if the persistent queue has issues.


209-210: LGTM!

The shared Gson instance in the companion object is efficient and ensures consistent serialization configuration.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookConverters.kt (1)

14-17: LGTM! Consider logging unknown status values.

The fallback to PENDING is sensible for data integrity. Optionally, you could log a warning when an unknown value is encountered to aid debugging schema migration issues, but this is not critical.

app/src/main/java/me/capcom/smsgateway/data/Module.kt (1)

9-9: LGTM!

The DI binding for webhookQueueDao follows the established pattern for other DAOs in this module.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/Module.kt (1)

3-3: LGTM!

The WebhookQueueRepository is properly registered as a singleton using the same pattern as WebHooksService.

Also applies to: 9-9

app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json (1)

330-414: Well-structured schema for the webhook queue.

The webhook_queue table design with composite indices on (status, next_attempt) and (status, created_at) is well-suited for queue processing queries.

app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt (1)

17-19: LGTM! Database schema changes are well-structured.

The new webhook queue entity is properly integrated: entity registered, version bumped, AutoMigration added, TypeConverters registered, and DAO accessor follows the established pattern.

Also applies to: 28-28, 31-31, 48-48, 51-51, 55-55

app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (1)

126-151: Well-structured webhook emission logic with proper guard clauses.

The refactored emit() method cleanly handles:

  • Disabled source filtering
  • Missing deviceId fallback with skip handling
  • Per-webhook error isolation with try-catch
  • Comprehensive logging for debugging
app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookStatus.kt (1)

8-38: Well-designed status enum with clear terminal/processing semantics.

The status lifecycle is well-defined:

  • FAILED allows retries (non-terminal)
  • PERMANENTLY_FAILED and COMPLETED are terminal states
  • Helper methods provide clean abstractions for status checks
app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueEntity.kt (1)

12-28: LGTM! Entity structure and indices are well-designed.

The composite indices on (status, next_attempt) and (status, created_at) align perfectly with the DAO query patterns for fetching pending webhooks and cleanup operations.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueDao.kt (2)

59-105: Well-designed state transition methods.

The DAO properly handles all webhook lifecycle states:

  • markAsFailed atomically increments retry count and schedules next attempt
  • markAsPermanentlyFailed captures the final error for debugging
  • Clear separation between recoverable (failed) and terminal (permanently_failed) states

151-161: LGTM! Statistics data class is well-structured.

The aggregate query and corresponding data class provide useful observability into queue health.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (5)

52-61: LGTM!

The dependency injection setup using Koin is clean and appropriate for this worker.


62-108: LGTM!

The companion object provides clean start/stop APIs with appropriate backoff configuration and network constraints.


110-172: LGTM!

The main processing loop correctly uses NonCancellable to ensure webhook processing completes even during cancellation, with proper logging and cleanup.


373-398: LGTM!

The failure handling with fallback to permanent failure ensures webhooks don't get stuck if retry scheduling fails.


400-409: LGTM!

The foreground info implementation follows the same pattern as SendWebhookWorker.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt (5)

10-26: LGTM!

Clean implementation for enqueuing webhooks with appropriate defaults.


28-40: LGTM!

Simple delegations to the DAO with appropriate defaults.


56-83: LGTM!

The retry logic correctly implements backoff scheduling and falls back to permanent failure when max retries exceeded. The caller handles exceptions appropriately.


120-133: LGTM!

Clean extension functions that encapsulate the retry and processing state logic.


95-109: LGTM!

Reasonable defaults for cleanup (7 days retention) and stuck webhook recovery (5 minutes timeout).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Dec 2, 2025

🤖 Pull request artifacts

file commit
app-release.apk af8c0f0
app-release.aab af8c0f0
app-insecure.apk af8c0f0
app-insecure.aab af8c0f0

@capcom6 capcom6 linked an issue Dec 2, 2025 that may be closed by this pull request
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (2)

359-361: Critical: client.close() in finally closes wrong HttpClient instance (duplicate issue).

This issue was flagged in previous reviews but remains unresolved. The client property is a getter that creates a new HttpClient on each access. Line 293's client.post(url) creates HttpClient #1, while line 360's client.close() creates HttpClient #2 and closes it. HttpClient #1 leaks.

Remove client.close() from the finally block:

-            } finally {
-                client.close()
-            }

The client lifecycle should be managed at the batch or worker level after fixing the client initialization (see separate comment on lines 405-423).


405-423: Critical: Client getter creates new HttpClient on every access, causing resource leaks (duplicate issue).

This issue was flagged in previous reviews but remains unresolved. The client property is a getter (get() = ...) that instantiates a new HttpClient every time it's accessed. Each unclosed client leaks connections, threads, and memory.

Use lazy initialization to create a single, reusable client:

-    private val client
-        get() = HttpClient(OkHttp) {
+    private val client by lazy {
+        HttpClient(OkHttp) {
             install(HttpTimeout) {
                 requestTimeoutMillis = PROCESSING_TIMEOUT_MS
                 connectTimeoutMillis = 5000
                 socketTimeoutMillis = 5000
             }
             install(ContentNegotiation) {
                 gson {
                     configure()
                 }
             }
             install(DefaultRequest) {
                 userAgent("${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME}")
             }
             install(PayloadSingingPlugin) {
                 secretKeyProvider = { settings.signingKey }
             }
         }
+    }

Then close the client once at the end of doWork() before returning success, or rely on worker termination to clean up.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d4cc7c9 and 3341ab7.

📒 Files selected for processing (1)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)
  • getForegroundInfo (167-169)
app/src/main/java/me/capcom/smsgateway/extensions/GsonBuilder.kt (1)
  • configure (7-9)
🔇 Additional comments (4)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (4)

47-108: LGTM: Clean worker setup with flexible start/stop API.

The class structure, dependency injection, and companion object API are well-designed. The REPLACE policy ensures only one processor runs at a time, and the optional network constraint provides flexibility.


174-281: LGTM: Robust batch processing with per-webhook error isolation.

The queue processing logic is well-structured with stuck webhook recovery, batch processing, and per-webhook error handling that prevents cascading failures. Good observability through detailed logging.


364-400: LGTM: Proper retry fallback and foreground notification setup.

The webhook failure handling correctly attempts retry scheduling with a fallback to permanent failure. The foreground notification setup follows standard patterns.


425-425: LGTM: Gson instance correctly initialized once.

The gson instance is created as a single instance and reused across all webhook sends, which is the correct approach.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (2)

368-370: Duplicate: client.close() in finally block is problematic.

As noted in past review comments, closing the client in the finally block is incorrect. Due to the client property being a getter (see lines 414-432), this creates a new HttpClient instance and closes it, leaving the actual client used on line 302 unclosed. Once the client property is fixed to use lazy initialization, this close call should be removed from here and moved to the end of batch processing or worker lifecycle.


414-432: Duplicate: Critical resource leak—client getter creates new instances on each access.

As detailed in past review comments, the client property is a getter that instantiates a new HttpClient every time it's accessed. On line 302, client.post(url) creates HttpClient #1, and on line 369, client.close() creates HttpClient #2 and closes it, leaving HttpClient #1 open and leaking connections/threads.

The fix is to use lazy initialization:

private val client by lazy {
    HttpClient(OkHttp) {
        install(HttpTimeout) {
            requestTimeoutMillis = PROCESSING_TIMEOUT_MS
            connectTimeoutMillis = 5000
            socketTimeoutMillis = 5000
        }
        install(ContentNegotiation) {
            gson {
                configure()
            }
        }
        install(DefaultRequest) {
            userAgent("${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME}")
        }
        install(PayloadSingingPlugin) {
            secretKeyProvider = { settings.signingKey }
        }
    }
}

Then remove the client.close() from the finally block in sendWebhook() and instead close the client once at the end of doWork() or when the worker completes.

🧹 Nitpick comments (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (2)

148-151: Optional: The fixed delay when idle may lead to unnecessary database polling.

When processedCount == 0, the worker delays 5 seconds then immediately checks hasScheduledWebhooks() again (which queries the database). If the queue remains empty for extended periods, this results in repeated DB queries every 5 seconds. Consider exiting the loop when the queue is empty and relying on external triggers (e.g., enqueuing new webhooks) to restart the worker.


334-355: Combine redundant exception handlers.

Both IllegalArgumentException and JSONException handlers perform identical actions (log error, return false). They can be combined for brevity.

Apply this diff:

-            } catch (e: IllegalArgumentException) {
+            } catch (e: Exception) {
+                when (e) {
+                    is IllegalArgumentException, is JSONException -> {
+                        logsSvc.insert(
+                            priority = LogEntry.Priority.ERROR,
+                            module = NAME,
+                            message = "Webhook validation error: ${e.message}",
+                            context = mapOf(
+                                "webhookId" to webhook.id,
+                                "error" to e.message
+                            )
+                        )
+                        false
+                    }
+                    else -> throw e
+                }
+            } catch (e: Throwable) {
                 logsSvc.insert(
-                    priority = LogEntry.Priority.ERROR,
+                    priority = LogEntry.Priority.WARN,
                     module = NAME,
-                    message = "Webhook invalid argument: ${e.message}",
+                    message = "Webhook network error: ${e.message}",
                     context = mapOf(
                         "webhookId" to webhook.id,
-                        "error" to e.message
+                        "error" to e.message,
+                        "retryCount" to webhook.retryCount
                     )
                 )
-                false
-            } catch (e: JSONException) {
-                logsSvc.insert(
-                    priority = LogEntry.Priority.ERROR,
-                    module = NAME,
-                    message = "Webhook JSON error: ${e.message}",
-                    context = mapOf(
-                        "webhookId" to webhook.id,
-                        "error" to e.message
-                    )
-                )
-                false
-            } catch (e: Throwable) {
-                logsSvc.insert(
-                    priority = LogEntry.Priority.WARN,
-                    module = NAME,
-                    message = "Webhook network error: ${e.message}",
-                    context = mapOf(
-                        "webhookId" to webhook.id,
-                        "error" to e.message,
-                        "retryCount" to webhook.retryCount
-                    )
-                )
-                false // Network errors should trigger retry
+                false
+            }

Or more simply:

-            } catch (e: IllegalArgumentException) {
-                logsSvc.insert(
-                    priority = LogEntry.Priority.ERROR,
-                    module = NAME,
-                    message = "Webhook invalid argument: ${e.message}",
-                    context = mapOf(
-                        "webhookId" to webhook.id,
-                        "error" to e.message
-                    )
-                )
-                false
-            } catch (e: JSONException) {
+            } catch (e: Exception) {
+                when (e) {
+                    is IllegalArgumentException, is JSONException -> {
                 logsSvc.insert(
                     priority = LogEntry.Priority.ERROR,
                     module = NAME,
-                    message = "Webhook JSON error: ${e.message}",
+                    message = "Webhook validation error: ${e.message}",
                     context = mapOf(
                         "webhookId" to webhook.id,
                         "error" to e.message
                     )
                 )
                 false
-            } catch (e: Throwable) {
+                    }
+                    else -> {
                 logsSvc.insert(
                     priority = LogEntry.Priority.WARN,
                     module = NAME,
                     message = "Webhook network error: ${e.message}",
                     context = mapOf(
                         "webhookId" to webhook.id,
                         "error" to e.message,
                         "retryCount" to webhook.retryCount
                     )
                 )
-                false // Network errors should trigger retry
+                        false
+                    }
+                }
+            }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3341ab7 and bc4739e.

📒 Files selected for processing (1)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)
  • getForegroundInfo (167-169)
app/src/main/java/me/capcom/smsgateway/extensions/GsonBuilder.kt (1)
  • configure (7-9)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build release / build-apk
  • GitHub Check: Build insecure / build-apk
🔇 Additional comments (5)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (5)

186-290: LGTM: Batch processing logic is well-structured.

The queue recovery, batch retrieval, and per-webhook processing flow are clearly implemented with appropriate error handling and observability.


376-398: LGTM: Retry handling with appropriate fallback.

The method correctly schedules retries with exponential backoff and falls back to marking the webhook as permanently failed if retry scheduling itself fails.


400-409: LGTM: Standard foreground service notification implementation.


434-434: LGTM: Gson properly initialized as a singleton instance.


1-60: LGTM: Clean class structure with proper dependency injection.

The imports are appropriate, and the class correctly extends CoroutineWorker with Koin-based dependency injection for services and repositories.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)

225-230: runBlocking blocks the calling thread.

start() is called from coroutine scopes in EventsReceiver, and runBlocking blocks the dispatcher thread. Consider making start() a suspend function or using a different approach.

Options to address this:

  1. Make start() a suspend function and update callers
  2. Use CoroutineScope(Dispatchers.IO).launch { ... } for fire-and-forget (but loses error propagation)
  3. Accept a CoroutineScope parameter for structured concurrency
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (1)

414-432: Critical: HttpClient getter still creates new instance on each access.

The client property uses get() = which instantiates a new HttpClient on every access. In sendWebhook():

  1. Line 302: client.post(url) creates HttpClient #1
  2. Line 369: client.close() creates HttpClient #2 and closes it

HttpClient #1 is never closed, causing connection/thread leaks across the batch.

-    private val client
-        get() = HttpClient(OkHttp) {
+    private val client by lazy {
+        HttpClient(OkHttp) {
             install(HttpTimeout) {
                 requestTimeoutMillis = PROCESSING_TIMEOUT_MS
                 connectTimeoutMillis = 5000
                 socketTimeoutMillis = 5000
             }
             install(ContentNegotiation) {
                 gson {
                     configure()
                 }
             }
             install(DefaultRequest) {
                 userAgent("${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME}")
             }
             install(PayloadSingingPlugin) {
                 secretKeyProvider = { settings.signingKey }
             }
         }
+    }

Then remove client.close() from the finally block in sendWebhook() (line 369) and add cleanup at the end of doWork() before returning success.

🧹 Nitpick comments (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt (1)

59-83: Consider handling missing webhook in scheduleRetry.

dao.getById(webhookId) at line 65 may throw if the webhook was deleted. The caller (handleWebhookFailure) catches exceptions, but explicit handling would be cleaner.

     suspend fun scheduleRetry(
         webhookId: Long,
         error: String?,
         maxRetries: Int = 3,
         baseDelayMs: Long = 5000L
     ) {
-        val webhook = dao.getById(webhookId)
+        val webhook = dao.getById(webhookId) ?: run {
+            // Webhook was deleted, nothing to retry
+            return
+        }

This requires changing getById to return WebhookQueueEntity? or keeping the current behavior where the exception propagates to handleWebhookFailure.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)

157-159: Catching Throwable includes unrecoverable JVM errors.

Similar to the issue fixed in WebhookQueueProcessorWorker, consider catching Exception instead.

-        } catch (e: Throwable) {
+        } catch (e: Exception) {
             e.printStackTrace()
             return Result.Retry(e.message ?: e.toString())
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc4739e and 9a68298.

📒 Files selected for processing (5)
  • app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueEntity.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (4 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)
  • getForegroundInfo (167-169)
app/src/main/java/me/capcom/smsgateway/extensions/GsonBuilder.kt (1)
  • configure (7-9)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build insecure / build-apk
  • GitHub Check: Build release / build-apk
🔇 Additional comments (6)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (3)

95-99: LGTM: ExistingWorkPolicy.KEEP prevents interrupting active processing.

Using KEEP ensures an already-running worker won't be cancelled mid-batch, preventing webhooks from getting stuck in "processing" state.


122-131: LGTM: setForeground wrapped in try-catch.

Properly handles ForegroundServiceStartNotAllowedException on Android 12+ without crashing the worker.


356-367: LGTM: Catching Exception instead of Throwable.

Properly catches recoverable exceptions while allowing critical JVM errors to propagate.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt (1)

7-26: LGTM: Clean repository pattern with proper initialization.

The repository correctly encapsulates DAO operations and initializes webhooks with PENDING status and current timestamp for immediate processing eligibility.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)

249-291: LGTM: Robust fallback mechanism.

The fallback to direct WorkManager processing when queue enqueue fails ensures webhooks aren't lost. Proper logging and storage of payload in TemporaryStorage maintains data integrity.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueEntity.kt (1)

12-44: LGTM: Well-designed entity with appropriate indexes.

The composite indexes on (status, next_attempt) and (status, created_at) optimize the query patterns for fetching pending webhooks and cleanup operations. The schema supports the retry/backoff mechanism effectively.

Comment thread app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json
@capcom6 capcom6 force-pushed the issue/286-optimize-webhooks-processing branch from 9a68298 to 12487c6 Compare December 3, 2025 23:24
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)

210-231: Avoid runBlocking inside start() to prevent blocking coroutine threads.

start() uses runBlocking to call the suspend enqueueWebhook(). When start() is invoked from coroutine-based flows (as in EventsReceiver/WebHooksService), this blocks the dispatcher thread and can hurt responsiveness or deadlock if used on a constrained dispatcher.

Prefer exposing a suspend API (e.g. suspend fun start(...)) or delegating enqueueing to a caller-owned CoroutineScope instead of runBlocking.

app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json (1)

331-414: Webhook queue schema matches the entity; revisit payload limits and cleanup indexing.

The webhook_queue table definition and its composite indices line up with WebhookQueueEntity and the DAO queries (status + next_attempt and status + created_at), so the schema is consistent.

Operationally, the earlier points still apply:

  • retry_count is tracked but max retries are enforced only in application logic; make sure this coupling is documented.
  • payload is an unbounded TEXT; consider validating payload size at enqueue time to avoid unbounded growth.
  • Cleanup queries rely on created_at; if you see heavy cleanup traffic, a dedicated created_at index (in addition to the composite one) could be worthwhile.
app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueDao.kt (2)

20-21: Make getById return nullable to avoid crashes on missing rows.

Declaring getById as returning a non-null WebhookQueueEntity means Room will throw if the ID is not found. Returning WebhookQueueEntity? lets callers handle “not found” gracefully.

@Query("SELECT * FROM webhook_queue WHERE id = :id")
suspend fun getById(id: Long): WebhookQueueEntity?

Then adjust callers to handle a null result.


48-57: Align markAsProcessing and recoverStuckProcessingWebhooks to avoid premature recovery.

recoverStuckProcessingWebhooks uses next_attempt < :timeoutThreshold to detect stuck items, but markAsProcessing only flips status = "processing" and leaves next_attempt unchanged. Under backlog, a webhook scheduled long ago but just started processing could immediately satisfy the timeout condition and be reset to pending while still in-flight.

Consider updating next_attempt when marking as processing so the timeout is measured from processing start, e.g.:

@Query(
    """
    UPDATE webhook_queue
    SET status = 'processing', next_attempt = :currentTime
    WHERE id = :id
    """
)
suspend fun markAsProcessing(
    id: Long,
    currentTime: Long = System.currentTimeMillis(),
)

Also applies to: 136-148

🧹 Nitpick comments (4)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)

48-53: Consider adding a Kotlin @Deprecated annotation in addition to KDoc.

You’ve documented deprecation in KDoc, but callers won’t get compiler warnings. Consider adding a @Deprecated annotation on SendWebhookWorker with a message and replacement (e.g. queue-based processing), so usage is surfaced at compile time.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (1)

126-210: Emit logic and logging are solid; consider unifying log module naming.

The new emit() flow correctly:

  • Filters webhooks by enabled source and available deviceId.
  • Builds WebHookEventDTO per webhook.
  • Queues via SendWebhookWorker.start() inside a per-item try/catch so one failure doesn’t short-circuit others.
  • Logs both per-webhook results and a final summary.

One small nit: here you use "WebHooksService" as the module value in logs, while other places (e.g. SendWebhookWorker) use NAME = "webhooks". Aligning on a single module identifier would make log querying/aggregation easier.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueEntity.kt (1)

12-44: Entity mapping is consistent; consider sharing a single timestamp source if needed.

WebhookQueueEntity matches the webhook_queue schema and indices well, and the defaults for retryCount, status, and timestamps are sensible.

If you ever need createdAt and nextAttempt to be exactly identical on initial insert, consider deriving both from a single timestamp in the repository/factory instead of separate System.currentTimeMillis() defaults in the constructor.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueDao.kt (1)

110-122: Guard queue statistics aggregates against NULL on empty tables.

getQueueStatistics() uses SUM(CASE WHEN ...) projections mapped into non-null Int fields. Depending on SQLite behavior, SUM(...) can return NULL when there are no rows, which may not map cleanly into non-null Kotlin Ints.

To be defensive, you could wrap each aggregate with IFNULL(..., 0) so statistics are always zero-based when the queue is empty.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a68298 and 12487c6.

📒 Files selected for processing (15)
  • app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt (3 hunks)
  • app/src/main/java/me/capcom/smsgateway/data/Module.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/Module.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (4 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookConverters.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueDao.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueEntity.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookStatus.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (4 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (1 hunks)
  • app/src/main/res/values-ru/strings.xml (6 hunks)
  • app/src/main/res/values-zh/strings.xml (1 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • app/src/main/java/me/capcom/smsgateway/data/Module.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookStatus.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookConverters.kt
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt
  • app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt
  • app/src/main/res/values-zh/strings.xml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build insecure / build-apk
  • GitHub Check: Build release / build-apk
🔇 Additional comments (4)
app/src/main/res/values/strings.xml (1)

85-85: String resource addition looks good.

The new processing_webhook_queue string is correctly placed near related webhook strings and follows project naming conventions. The ellipsis usage aligns with the pattern for ongoing operations throughout the file. All corresponding localized strings have been added: Chinese locale contains "处理 Webhook 队列中…" and Russian locale contains "Обработка очереди вебхуков…".

app/src/main/java/me/capcom/smsgateway/modules/webhooks/Module.kt (1)

3-10: DI binding for WebhookQueueRepository looks correct.

Singleton registration via singleOf(::WebhookQueueRepository) is consistent with the existing WebHooksService binding and should work fine with Koin injection.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)

249-291: Fallback path on enqueue failure is well thought out.

On repository enqueue failure you:

  • Log a WARN with error details and URL.
  • Persist the payload to TemporaryStorage under a stable key (data.id).
  • Recreate the original SendWebhookWorker OneTimeWorkRequest with backoff, expedited policy, and optional network constraint.

This preserves delivery behavior even if the new queue infrastructure fails during enqueue.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (1)

36-45: Queue processor lifecycle wiring looks good.

Starting WebhookQueueProcessorWorker alongside ReviewWebhooksWorker and stopping it in stop() keeps the processor aligned with the webhook module’s lifecycle and should avoid orphaned workers.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (3)

163-168: Consider passing Context as parameter for consistency.

The emit method uses get<Context>() to retrieve Context from Koin, while start/stop methods receive Context as a parameter. For consistency and easier testing, consider passing Context to emit() instead.

-    fun emit(event: WebHookEvent, payload: Any) {
+    fun emit(context: Context, event: WebHookEvent, payload: Any) {
         val webhooksToProcess = webHooksDao.selectByEvent(event)
         var queuedCount = 0
         var skippedCount = 0
 
         webhooksToProcess.forEach { webhook ->
             // ... skip logic ...
 
             try {
                 val webhookEventDTO = WebHookEventDTO(
                     id = NanoIdUtils.randomNanoId(),
                     webhookId = webhook.id,
                     event = event,
                     deviceId = deviceId,
                     payload = payload,
                 )
 
                 SendWebhookWorker.start(
-                    context = get<Context>(),
+                    context = context,
                     url = webhook.url,
                     data = webhookEventDTO,
                     internetRequired = webhooksSettings.internetRequired
                 )

172-181: Consider reducing logging verbosity for normal operations.

The per-webhook success log at DEBUG level may generate excessive logs when many webhooks are configured. Consider removing individual success logs and relying on the summary, or making them conditional on a verbose debug flag.

-                logsService.insert(
-                    LogEntry.Priority.DEBUG,
-                    NAME,
-                    "Queued webhook event for processing",
-                    mapOf(
-                        "webhookId" to webhook.id,
-                        "event" to event.name,
-                        "internetRequired" to webhooksSettings.internetRequired
-                    )
-                )
+                // Individual success logs removed; rely on summary

197-210: Log summary at higher priority when webhooks are skipped.

The summary always logs at DEBUG level, even when webhooks fail. Consider logging at WARN or ERROR when skippedCount > 0 to make failures more visible in production logs.

         // Log summary for debugging
         if (webhooksToProcess.isNotEmpty()) {
+            val priority = if (skippedCount > 0) LogEntry.Priority.WARN else LogEntry.Priority.DEBUG
             logsService.insert(
-                LogEntry.Priority.DEBUG,
+                priority,
                 NAME,
                 "Webhook emission summary",
                 mapOf(
                     "event" to event.name,
                     "totalWebhooks" to webhooksToProcess.size,
                     "queued" to queuedCount,
                     "skipped" to skippedCount
                 )
             )
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12487c6 and 17db4ab.

📒 Files selected for processing (2)
  • app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt (3 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build release / build-apk
  • GitHub Check: Build insecure / build-apk
🔇 Additional comments (8)
app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt (4)

17-18: Webhook queue imports look consistent

The new imports for WebhookQueueDao and WebhookQueueEntity align with the additions below and avoid fully qualified usage; no issues here.


21-29: Entity registration for WebhookQueueEntity is appropriate

Adding WebhookQueueEntity to the entities array is the right way to bring the webhook queue table under Room management. Assuming its fields either use existing Converters or are annotated with their own @TypeConverters, this setup should be sufficient.

Please double‑check that WebhookQueueEntity compiles cleanly with the current @TypeConverters(Converters::class) configuration (or its own per‑entity converters) and that no additional database‑wide converters are required.


30-48: Version bump and auto‑migration path 16→17

Bumping the DB version to 17 and declaring AutoMigration(from = 16, to = 17) keeps the migration chain continuous alongside the existing manual migrations (7→8, 13→14). For a new queue table, an auto‑migration is typically appropriate and should be generated by Room.

Please confirm that:

  • Room successfully generates the 16→17 auto‑migration during compilation, and
  • A test/device upgrade from an existing v16 database to v17 runs without migration errors (e.g., via your existing migration test setup or a manual upgrade on a debug build).

52-55: DAO accessor for WebhookQueueDao is correctly exposed

Adding abstract fun webhookQueueDao(): WebhookQueueDao matches the new DAO and is consistent with the other DAO accessors.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (4)

11-12: LGTM! Dependencies correctly added for queue-based webhook processing.

The LogsService and WebhookQueueProcessorWorker imports, along with the constructor parameter, properly support the new queuing architecture.

Also applies to: 21-21, 32-32


126-151: LGTM! Multi-webhook processing with proper skip logic.

The rewritten emit method correctly processes all webhooks for an event, with appropriate skip logic for disabled sources and missing device IDs.


36-46: Worker lifecycle management is correctly implemented.

The WebhookQueueProcessorWorker.start() method properly accepts both context and internetRequired parameters, and correctly applies network constraints when required (lines 85-91). The stop() method properly cancels the worker through WorkManager. Queue processing with batch handling, retry logic, and error handling is complete.


153-194: No action needed. SendWebhookWorker persists webhook events to the database queue before any potential failure points. The start() method (lines 225-230) enqueues to the persistent queue immediately, and includes a fallback mechanism with exponential backoff if queue enqueue fails (lines 265-277). Exception handling in WebHooksService lines 182-194 properly logs errors from the start() call itself, but the webhook data is already safely persisted by that point.

Likely an incorrect or invalid review comment.

@capcom6 capcom6 force-pushed the issue/286-optimize-webhooks-processing branch from 05189c9 to af8c0f0 Compare December 4, 2025 03:09
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (6)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)

225-230: runBlocking blocks a coroutine dispatcher thread.

This concern was already raised in a previous review. The start() function is invoked from coroutine scopes in EventsReceiver, so using runBlocking here blocks a dispatcher thread unnecessarily. Consider making start() a suspend function or using a different approach to avoid blocking.

app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json (1)

331-414: Webhook queue table schema is well-designed.

The webhook_queue table structure is appropriate for the persistent queue pattern:

  • Auto-increment primary key for ordering
  • Proper fields for retry tracking (retry_count, next_attempt, last_error)
  • Status field with sensible default ('pending')
  • Composite indices on (status, next_attempt) and (status, created_at) align with expected query patterns

Previous review comments regarding retry limit enforcement at the application level, payload size validation, and cleanup query optimization remain valid considerations for future iterations.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueDao.kt (2)

20-21: getById will crash if the record doesn't exist.

Room will throw an exception if no row matches the ID when the return type is non-nullable. Return nullable to handle missing records gracefully.

Apply this diff:

     @Query("SELECT * FROM webhook_queue WHERE id = :id")
-    suspend fun getById(id: Long): WebhookQueueEntity
+    suspend fun getById(id: Long): WebhookQueueEntity?

48-57: Recovery logic measures timeout from schedule time, not processing start.

markAsProcessing() doesn't update next_attempt, so recoverStuckProcessingWebhooks() at lines 136-148 checks next_attempt < :timeoutThreshold against when webhooks were scheduled, not when they started processing. This can incorrectly recover webhooks that just started.

Update markAsProcessing to set next_attempt to current time:

     @Query(
         """
         UPDATE webhook_queue 
-        SET status = "processing" 
+        SET status = 'processing', next_attempt = :currentTime
         WHERE id = :id
     """
     )
     suspend fun markAsProcessing(
         id: Long,
+        currentTime: Long = System.currentTimeMillis()
     )
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (2)

414-432: Critical: client getter creates new HttpClient on each access, causing resource leaks.

The client property is a getter that instantiates a new HttpClient every time it's accessed:

  • Line 302: client.post(url) creates HttpClient #1
  • Line 369: client.close() creates HttpClient #2 and closes it

HttpClient #1 is never closed, leaking connections and threads.

Use lazy initialization:

-    private val client
-        get() = HttpClient(OkHttp) {
+    private val client by lazy {
+        HttpClient(OkHttp) {
             install(HttpTimeout) {
                 requestTimeoutMillis = PROCESSING_TIMEOUT_MS
                 connectTimeoutMillis = 5000
                 socketTimeoutMillis = 5000
             }
             install(ContentNegotiation) {
                 gson {
                     configure()
                 }
             }
             install(DefaultRequest) {
                 userAgent("${BuildConfig.APPLICATION_ID}/${BuildConfig.VERSION_NAME}")
             }
             install(PayloadSingingPlugin) {
                 secretKeyProvider = { settings.signingKey }
             }
         }
+    }

Then remove the finally block from sendWebhook() (see next comment) and optionally close the client once at the end of doWork() after line 166:

// Before return@withContext Result.success() at line 166
if (::client.isInitialized) {
    client.close()
}

368-370: Remove client.close() from the finally block.

Once the client is lazily initialized (per the previous comment), closing it here would prematurely terminate the client for subsequent webhooks in the batch. Remove this finally block entirely.

Apply this diff:

                 false // Network errors should trigger retry
-            } finally {
-                client.close()
-            }
+            }
         }
🧹 Nitpick comments (1)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)

210-210: Consider reusing Gson instance from class scope.

The gson instance in the companion object duplicates the pattern used in sendData() (line 138). Consider consolidating to a single shared instance to avoid redundant configuration.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17db4ab and af8c0f0.

📒 Files selected for processing (17)
  • app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt (3 hunks)
  • app/src/main/java/me/capcom/smsgateway/data/Module.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/EventsReceiver.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/Module.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (4 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookConverters.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueDao.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueEntity.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookStatus.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (4 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (1 hunks)
  • app/src/main/res/values-ru/strings.xml (6 hunks)
  • app/src/main/res/values-zh/strings.xml (1 hunks)
  • app/src/main/res/values/strings.xml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/java/me/capcom/smsgateway/data/Module.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueEntity.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/Module.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookStatus.kt
  • app/src/main/res/values/strings.xml
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookConverters.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/db/WebhookQueueRepository.kt
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (2)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (1)
  • getForegroundInfo (167-169)
app/src/main/java/me/capcom/smsgateway/extensions/GsonBuilder.kt (1)
  • configure (7-9)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build insecure / build-apk
  • GitHub Check: Build release / build-apk
🔇 Additional comments (11)
app/src/main/res/values-zh/strings.xml (1)

88-88: Localization addition looks good.

The new Chinese string for webhook queue processing is correctly formatted, properly positioned, and the translation aligns with the pattern of similar UI status strings (e.g., "sending_webhook" on line 87).

app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt (1)

17-18: LGTM! Database extension for webhook queue is well-structured.

The Room database changes correctly add the new WebhookQueueEntity to the entities list, bump the version to 17, and use AutoMigration for the 16→17 transition. Since the webhook_queue table is entirely new with no column alterations to existing tables, AutoMigration is appropriate here.

Also applies to: 27-27, 30-30, 47-47, 54-54

app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (1)

102-102: LGTM! Updated emit call with context parameter.

The change correctly passes the context parameter (already available in process()) to the updated WebHooksService.emit signature, enabling context-aware webhook queue processing.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/EventsReceiver.kt (2)

70-72: Same Context injection pattern - verify consistency.

This get() call uses the same pattern as line 24. The verification above applies here as well.


23-27: Context is properly registered in the Koin DI container via androidContext(this@App) in App.kt (line 33). The get() call correctly resolves to the registered Application context through type inference from the emit() function's first parameter (Context). No NoBeanDefFoundException will be thrown at runtime.

Likely an incorrect or invalid review comment.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/SendWebhookWorker.kt (2)

48-51: Good practice: Clear deprecation notice with removal date.

The @deprecated annotation with a specific removal date (2026-11-30) provides clear guidance for future cleanup. This is helpful for tracking technical debt.


249-291: Good fallback mechanism for queue enqueue failures.

The fallback path that stores the payload in TemporaryStorage and uses OneTimeWorkRequest provides resilience if the persistent queue fails. The logging at WARN level appropriately signals the degraded path.

app/schemas/me.capcom.smsgateway.data.AppDatabase/17.json (1)

1-5: Schema version 17 snapshot looks complete.

The schema file correctly captures the full database state at version 17, including the new webhook_queue entity and all existing tables with their indices and foreign keys.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/WebHooksService.kt (1)

126-210: LGTM! Queue-based emission is well-implemented.

The refactored emit method correctly:

  • Processes webhooks in a loop with proper skip logic for disabled sources
  • Resolves deviceId with fallback handling
  • Queues each webhook via SendWebhookWorker with comprehensive error handling
  • Logs per-webhook and summary statistics for debugging

The try-catch per webhook ensures one failure doesn't block others, and the counters provide good observability.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/workers/WebhookQueueProcessorWorker.kt (2)

110-180: LGTM! Worker lifecycle and error handling are well-structured.

The doWork() method correctly:

  • Wraps foreground service setup in try-catch (lines 122-131) to handle Android 12+ restrictions
  • Uses NonCancellable context for queue processing to prevent partial state
  • Implements backoff delay when queue is empty
  • Cleans up old entries before completion
  • Returns Result.retry() with linear backoff on exceptions

95-99: Good choice using ExistingWorkPolicy.KEEP.

Using KEEP prevents interrupting an active worker mid-batch, which would leave webhooks stuck in "processing" state. This is the correct policy for this use case.

@capcom6 capcom6 added the ready label Dec 4, 2025
@capcom6 capcom6 merged commit 8d81c5b into master Dec 5, 2025
3 checks passed
@capcom6 capcom6 deleted the issue/286-optimize-webhooks-processing branch December 5, 2025 23:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Webhooks errors

1 participant