{"record":{"id":"0ef346ff52eb178b","repo":"Freika/dawarich","slug":"database-error-please-try-again","errorCode":null,"errorMessage":"Database error: please try again.","messagePattern":"Database error: please try again\\.","errorType":"http","errorClass":null,"httpStatus":422,"severity":"error","filePath":"app/services/visits/bulk_destroy.rb","lineNumber":59,"sourceCode":"      place_ids = visits.reorder(nil).where.not(place_id: nil).distinct.pluck(:place_id)\n      started_at = Time.current\n\n      # Soft delete: rows stay as tombstones (points and place links intact)\n      # so visit detection never re-suggests what the user removed.\n      Visit.where(id: ids).update_all(deleted_at: Time.current)\n\n      # update_all skips AR callbacks, so the orphan-place check the model\n      # runs on single soft deletes must be enqueued here by hand.\n      place_ids.each { |place_id| Places::DeleteIfOrphanJob.perform_later(place_id) }\n\n      log_success(ids.length, Time.current - started_at)\n\n      { count: ids.length, started_ats: started_ats }\n    rescue ActiveRecord::StatementInvalid => e\n      Rails.logger.warn(\n        \"Visits::BulkDestroy failed user_id=#{user.id} count=#{ids&.length} error=#{e.class}: #{e.message}\"\n      )\n      errors << I18n.t('services.visits.bulk_destroy.database_error')\n      false\n    end\n\n    def log_success(count, duration_seconds)\n      Rails.logger.info(\n        \"Visits::BulkDestroy user_id=#{user.id} count=#{count} duration_ms=#{(duration_seconds * 1000).round}\"\n      )\n    end\n  end\nend\n","sourceCodeStart":41,"sourceCodeEnd":70,"githubUrl":"https://github.com/Freika/dawarich/blob/97fad417c5a11b0eb11157890635e015723a2e97/app/services/visits/bulk_destroy.rb#L41-L70","documentation":"Visits::BulkDestroy runs a bulk soft delete of many visit ids against PostgreSQL and rescues ActiveRecord::StatementInvalid — the wrapper Rails raises when the database rejects a statement (deadlock, lock wait timeout, statement timeout, dropped connection, constraint violation). The service logs the error class and message with the user id and count, appends the localized 'Database error: please try again.' message to errors, and returns false so the caller surfaces it.","triggerScenarios":"Deleting a very large batch of visits producing SQL that exceeds statement_timeout or complexity limits; a concurrent job (points import, visit recalculation) holding row locks so the bulk update hits lock wait timeout or deadlock; the DB connection dropping mid-transaction; constraint failures after schema drift.","commonSituations":"Users selecting months of visits to delete at once; background imports running simultaneously for the same user; managed Postgres with a low statement_timeout; overlapping bulk operations on the same rows.","solutions":["Read the Rails warn line — the error class and PG message name the exact cause (deadlock detected vs lock wait timeout vs statement timeout)","Reduce the batch size: destroy in chunks of a few hundred ids per statement","Retry once after a short pause for deadlock/lock-timeout classes — they are transient","If statement_timeout is the cause, raise it for this job or move the work to a background worker","Reschedule bulk deletes so they do not overlap imports or recalculations for the same user"],"exampleFix":"# before\nVisit.where(id: ids).update_all(deleted_at: Time.current)\n# after — smaller statements, shorter row locks\nids.each_slice(500) do |slice|\n  Visit.where(id: slice).update_all(deleted_at: Time.current)\nend","handlingStrategy":"retry","validationCode":"return false if ids.blank?\nraise ArgumentError, 'batch too large' if ids.length > 5000 # force client-side chunking\nchunked = ids.each_slice(500).to_a","typeGuard":null,"tryCatchPattern":"retries = 0\nbegin\n  Visit.where(id: ids).update_all(deleted_at: Time.current)\nrescue ActiveRecord::StatementInvalid => e\n  retries += 1\n  retry if retries <= 2 && e.message =~ /deadlock|lock wait timeout/i\n  Rails.logger.warn(\"Visits::BulkDestroy failed user_id=#{user.id} error=#{e.class}: #{e.message}\")\n  errors << I18n.t('services.visits.bulk_destroy.database_error')\n  false\nend","preventionTips":["Chunk bulk updates so each statement stays well under statement_timeout","Run bulk destroys in a background job away from request timeouts","Retry deadlock and lock-timeout errors with jitter — they are usually transient","Avoid scheduling bulk deletes concurrently with imports for the same user"],"tags":["rails","activerecord","postgresql","bulk-delete","visits","deadlock"],"backgroundTag":"activerecord-statement-invalid","analyzedSha":"97fad417c5a11b0eb11157890635e015723a2e97","analyzedAt":"2026-08-21T17:04:17.778Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}