-
-
Notifications
You must be signed in to change notification settings - Fork 6.4k
Fix concurrent stat insertion duplicate key errors log message #5933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Resolves issue where multiple monitors updating statistics simultaneously can cause "Duplicate entry" database errors for the same monitor_id and timestamp combination in stat_hourly and stat_daily tables. Changes: - Add database-specific upsert logic for SQLite and MariaDB - Replace R.store() calls with atomic upsert operations - Add fallback to original R.store() if upsert fails - Initialize default values for new stat beans to prevent null conflicts - Use ON CONFLICT/ON DUPLICATE KEY UPDATE for atomic stat updates This fix is particularly important for high-volume monitoring scenarios with 400+ monitors where concurrent heartbeats can trigger race conditions in the stat insertion process. Fixes louislam#5357
This commit resolves duplicate key errors that occur when multiple monitors attempt to insert statistics simultaneously into stat_* tables. The fix addresses three interconnected issues: 1. **Circular Dependency Resolution**: database.js imports UptimeCalculator at module level, but UptimeCalculator needs Database.dbConfig. Fixed by using local imports in UptimeCalculator methods to ensure Database.dbConfig is properly initialized when accessed. 2. **Database Configuration Initialization**: Database.dbConfig was not set in the catch block when db-config.json is missing, causing undefined access errors. Fixed by ensuring Database.dbConfig is always set. 3. **Schema Column Naming Mismatch**: RedBean ORM uses camelCase (pingMin/pingMax) but Knex migrations create snake_case columns (ping_min/ping_max). Fixed by using correct snake_case column names in SQL queries. 4. **Atomic Upsert Operations**: Implemented database-specific upsert logic: - SQLite: INSERT ... ON CONFLICT DO UPDATE - MariaDB: INSERT ... ON DUPLICATE KEY UPDATE The solution maintains backward compatibility by falling back to R.store() when upsert fails, ensuring no data loss while eliminating race conditions for users with many monitors (200+). Fixes louislam#5357
I don't like the solution here.. feels like a horrible hack. |
@CommanderStorm I afraid that could be hard too, because RedBeanNode underlying is also not using raw sql. It is using But before that, I would like to have a simple reproduce steps (or simple PoC), because I am still not fully understand. In Uptime Kuma, Uptime Kuma's getDailyStatBean(): async getDailyStatBean(timestamp) {
if (this.lastDailyStatBean && this.lastDailyStatBean.timestamp === timestamp) {
return this.lastDailyStatBean;
} RedBeanNode's R.store(): async store(bean: Bean, changedFieldsOnly = true) {
await bean.beanMeta.lock.acquireAsync(); |
Just checked knex's doc, it has a function for upsert: |
📋 Overview
What problem does this pull request address?
Users with many monitors (200+) frequently encounter database duplicate key errors when multiple monitors attempt to insert statistics simultaneously. This manifests as
Error: Duplicate entry '1-1703123456' for key 'monitor_id'
and occurs because the current implementation uses RedBean'sR.store()
which performs separate SELECT and INSERT/UPDATE operations, creating race conditions when multiple monitors update statistics for the same time period.What features or functionality does this pull request introduce or enhance?
Implements atomic upsert operations for stat table insertions using database-specific SQL syntax (SQLite
ON CONFLICT
and MariaDBON DUPLICATE KEY UPDATE
). Also resolves related circular dependency and schema naming issues that prevent the upsert functionality from working correctly.🔄 Changes
🛠️ Type of change
🔗 Related Issues
insert into 'stat_hourly' - Duplicate entry '43-1731949200' for key 'stat_hourly_monitor_id_timestamp_unique'
#5357📄 Checklist
ℹ️ Additional Context
Key Considerations:
Design decisions – Used database-specific atomic upsert operations rather than application-level locking to ensure maximum performance and reliability. Falls back to original
R.store()
behavior if upsert fails, ensuring no data loss.Alternative solutions – Considered application-level mutex/locking but atomic database operations are more efficient and reliable for this use case.
Dependencies – Required fixing circular dependency between
database.js
anduptime-calculator.js
, and ensuringDatabase.dbConfig
is properly initialized in all code paths.Technical Details:
The fix addresses several interconnected issues:
database.js
importsUptimeCalculator
, butUptimeCalculator
needsDatabase.dbConfig
Database.dbConfig
wasn't being set when db-config.json is missingpingMin
) but the actual database schema uses snake_case (ping_min
)Testing performed:
Backward compatibility: No breaking changes. If upsert fails for any reason, gracefully falls back to the original
R.store()
approach with appropriate logging.