Freika/dawarich · error

Database error: please try again.

Error message

Database error: please try again.

What it means

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.

Source

Thrown at app/services/visits/bulk_destroy.rb:59

      place_ids = visits.reorder(nil).where.not(place_id: nil).distinct.pluck(:place_id)
      started_at = Time.current

      # Soft delete: rows stay as tombstones (points and place links intact)
      # so visit detection never re-suggests what the user removed.
      Visit.where(id: ids).update_all(deleted_at: Time.current)

      # update_all skips AR callbacks, so the orphan-place check the model
      # runs on single soft deletes must be enqueued here by hand.
      place_ids.each { |place_id| Places::DeleteIfOrphanJob.perform_later(place_id) }

      log_success(ids.length, Time.current - started_at)

      { count: ids.length, started_ats: started_ats }
    rescue ActiveRecord::StatementInvalid => e
      Rails.logger.warn(
        "Visits::BulkDestroy failed user_id=#{user.id} count=#{ids&.length} error=#{e.class}: #{e.message}"
      )
      errors << I18n.t('services.visits.bulk_destroy.database_error')
      false
    end

    def log_success(count, duration_seconds)
      Rails.logger.info(
        "Visits::BulkDestroy user_id=#{user.id} count=#{count} duration_ms=#{(duration_seconds * 1000).round}"
      )
    end
  end
end

View on GitHub (pinned to 97fad417c5)

Solutions

  1. Read the Rails warn line — the error class and PG message name the exact cause (deadlock detected vs lock wait timeout vs statement timeout)
  2. Reduce the batch size: destroy in chunks of a few hundred ids per statement
  3. Retry once after a short pause for deadlock/lock-timeout classes — they are transient
  4. If statement_timeout is the cause, raise it for this job or move the work to a background worker
  5. Reschedule bulk deletes so they do not overlap imports or recalculations for the same user

Example fix

# before
Visit.where(id: ids).update_all(deleted_at: Time.current)
# after — smaller statements, shorter row locks
ids.each_slice(500) do |slice|
  Visit.where(id: slice).update_all(deleted_at: Time.current)
end
Defensive patterns

Strategy: retry

Validate before calling

return false if ids.blank?
raise ArgumentError, 'batch too large' if ids.length > 5000 # force client-side chunking
chunked = ids.each_slice(500).to_a

Try / catch

retries = 0
begin
  Visit.where(id: ids).update_all(deleted_at: Time.current)
rescue ActiveRecord::StatementInvalid => e
  retries += 1
  retry if retries <= 2 && e.message =~ /deadlock|lock wait timeout/i
  Rails.logger.warn("Visits::BulkDestroy failed user_id=#{user.id} error=#{e.class}: #{e.message}")
  errors << I18n.t('services.visits.bulk_destroy.database_error')
  false
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Freika/dawarich@97fad417c5 (2026-08-21). Data as JSON: /api/errors/0ef346ff52eb178b. Report an issue: GitHub.