diff --git a/.env.example b/.env.example index ec16f9c..625f725 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ # Generate using: npx web-push generate-vapid-keys VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY= -VAPID_SUBJECT=mailto:mailto:admin@virtonline.eu +VAPID_SUBJECT=mailto:mailto:user@example.org # --- Server Configuration --- # Internal port for the Node.js app @@ -32,9 +32,11 @@ ALLOWED_HEADERS=Content-Type,Authorization # --- Web Push Retry Configuration (Optional) --- # Number of retries on failure (e.g., DNS issues) -MAX_NOTIFICATION_RETRIES=3 -# Initial delay in milliseconds -INITIAL_RETRY_DELAY_MS=1000 +NOTIFICATION_MAX_RETRIES=3 +# First retry delay in milliseconds (minimal delay for immediate retry) +NOTIFICATION_FIRST_RETRY_DELAY_MS=10 +# Base delay in milliseconds for subsequent retries (used for exponential backoff) +NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS=1000 # --- Network Configuration (Optional) --- # Timeout for DNS lookups (ms) diff --git a/README.md b/README.md index de7a074..99ce048 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,9 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for * `ALLOWED_ORIGINS`: Comma-separated list of domains allowed by CORS. Include your PWA's domain if it needs to interact directly (e.g., for setup). Example: `https://my-pwa.com`. * `ALLOWED_METHODS`: (Default: `POST,OPTIONS`) Standard methods needed. * `ALLOWED_HEADERS`: (Default: `Content-Type,Authorization`) Standard headers needed. - * `MAX_NOTIFICATION_RETRIES`: (Default: `3`) Number of retry attempts for failed push notifications. Must be a number. - * `INITIAL_RETRY_DELAY_MS`: (Default: `1000`) Initial delay in milliseconds before first retry. Must be a number. + * `NOTIFICATION_MAX_RETRIES`: (Default: `3`) Number of retry attempts for failed push notifications. Must be a number. + * `NOTIFICATION_FIRST_RETRY_DELAY_MS`: (Default: `10`) Delay in milliseconds for the first retry attempt. Setting to 0-10ms provides near-immediate first retry for transient DNS issues. Must be a number. + * `NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS`: (Default: `1000`) Base delay in milliseconds for subsequent retries. Each additional retry uses this value with exponential backoff and jitter. Must be a number. * `DNS_TIMEOUT_MS`: (Default: `5000`) DNS resolution timeout in milliseconds. Must be a number. * `HTTP_TIMEOUT_MS`: (Default: `10000`) HTTP request timeout in milliseconds. Must be a number. * `LOG_LEVEL`: (Default: `info`) Controls verbosity of logs. Valid values are `error`, `warn`, `info`, or `debug`. Use `debug` to see detailed header information and other diagnostic messages. @@ -245,4 +246,10 @@ If you receive a different response, refer to the Troubleshooting section below. * **Verify `labels`:** Double-check that variables were correctly substituted manually and match your `.env` and Traefik setup. * **Verify `subscriptions.json`:** Ensure it's valid JSON and the button serial number (key) matches exactly what Flic sends (check backend logs for "Received webhook: Button=..."). Check if the subscription details are correct. Case sensitivity matters for the JSON keys (button serials). * **Check Flic Configuration:** Ensure the URL, Method, `click_type` parameter, and authentication details (Username/Password if enabled) are correct in the Flic action setup. Use `curl` or Postman to test the endpoint manually first. -* **PWA Service Worker:** Remember that the PWA needs a correctly registered Service Worker to receive and handle the incoming push messages. Ensure the PWA subscribes using the *same* `VAPID_PUBLIC_KEY` configured in the backend's `.env`. \ No newline at end of file +* **PWA Service Worker:** Remember that the PWA needs a correctly registered Service Worker to receive and handle the incoming push messages. Ensure the PWA subscribes using the *same* `VAPID_PUBLIC_KEY` configured in the backend's `.env`. +* **Push Notification Retry Mechanism:** The service includes an optimized retry mechanism for handling temporary DNS resolution issues: + * First retry happens immediately or with minimal delay (controlled by `NOTIFICATION_FIRST_RETRY_DELAY_MS`, default 10ms) + * Subsequent retries use exponential backoff with jitter (starting from `NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS`, default 1000ms) + * Maximum number of retries is controlled by `NOTIFICATION_MAX_RETRIES` (default 3) + * This approach minimizes latency for transient DNS issues while preventing excessive requests for persistent problems + * Adjust these values in your `.env` file based on your network conditions and reliability requirements \ No newline at end of file diff --git a/server.js b/server.js index 6a37b12..0135351 100644 --- a/server.js +++ b/server.js @@ -24,8 +24,9 @@ const allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(',').map(origin const allowedMethods = (process.env.ALLOWED_METHODS || "POST,OPTIONS,GET").split(',').map(method => method.trim()).filter(method => method); const allowedHeaders = (process.env.ALLOWED_HEADERS || "Content-Type,Authorization").split(',').map(header => header.trim()).filter(header => header); // Retry configuration for DNS resolution issues -const maxRetries = parseInt(process.env.MAX_NOTIFICATION_RETRIES || 3, 10); -const initialRetryDelay = parseInt(process.env.INITIAL_RETRY_DELAY_MS || 1000, 10); // 1 second +const maxRetries = parseInt(process.env.NOTIFICATION_MAX_RETRIES || 3, 10); +const subsequentRetryDelay = parseInt(process.env.NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS || 1000, 10); // 1 second base delay for subsequent retries +const firstRetryDelay = parseInt(process.env.NOTIFICATION_FIRST_RETRY_DELAY_MS || 10, 10); // 10 milliseconds - minimal delay for first retry // HTTP request timeout configuration const httpTimeout = parseInt(process.env.HTTP_TIMEOUT_MS || 10000, 10); // 10 seconds // Logging level configuration @@ -98,7 +99,7 @@ dns.setServers(dns.getServers()); // Reset DNS servers (can help in some Docker // Example: dns.setServers(['8.8.8.8', '1.1.1.1']); // --- Utility function for retrying web push notifications with exponential backoff --- -async function sendWebPushWithRetry(subscription, payload, retryCount = 0, delay = initialRetryDelay) { +async function sendWebPushWithRetry(subscription, payload, retryCount = 0, delay = subsequentRetryDelay) { try { return await webpush.sendNotification(subscription, payload); } catch (error) { @@ -108,13 +109,25 @@ async function sendWebPushWithRetry(subscription, payload, retryCount = 0, delay error.code === 'ETIMEDOUT'; if (isDnsError && retryCount < maxRetries) { - console.log(`DNS resolution failed (${error.code}). Retrying notification in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})...`); + // For first retry (retryCount = 0), use minimal delay or no delay + const actualDelay = retryCount === 0 ? firstRetryDelay : delay; - // Wait for the delay - await new Promise(resolve => setTimeout(resolve, delay)); + if (retryCount === 0) { + logger.info(`DNS resolution failed (${error.code}). Retrying notification immediately or with minimal delay of ${firstRetryDelay}ms (attempt ${retryCount + 1}/${maxRetries})...`); + } else { + logger.info(`DNS resolution failed (${error.code}). Retrying notification in ${actualDelay}ms (attempt ${retryCount + 1}/${maxRetries})...`); + } - // Exponential backoff with jitter - const nextDelay = delay * (1.5 + Math.random() * 0.5); + // Wait for the delay (minimal or none for first retry) + if (actualDelay > 0) { + await new Promise(resolve => setTimeout(resolve, actualDelay)); + } + + // Calculate next delay with exponential backoff + jitter + // First retry uses subsequentRetryDelay, subsequent retries use exponential increase + const nextDelay = retryCount === 0 ? + subsequentRetryDelay : + delay * (1.5 + Math.random() * 0.5); // Retry recursively with increased count and delay return sendWebPushWithRetry(subscription, payload, retryCount + 1, nextDelay); @@ -360,7 +373,7 @@ server.listen(port, () => { } logger.info(`Subscription Endpoint Auth: ${basicAuthUsername && basicAuthPassword ? 'Enabled (Basic)' : 'Disabled'}`); logger.info(`Subscriptions File: ${subscriptionsFilePath}`); - logger.info(`Push Notification Retry Config: ${maxRetries} retries, ${initialRetryDelay}ms initial delay`); + logger.info(`Push Notification Retry Config: ${maxRetries} retries, first retry: ${firstRetryDelay}ms, subsequent retries: ${subsequentRetryDelay}ms base delay`); logger.info(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`); logger.info(`HTTP Timeout: ${httpTimeout}ms`); logger.info(`Log Level: ${LOG_LEVEL.toUpperCase()}`);