SQLite3 :: BusyException
-
09-06-2019 - |
Question
Exécution d'un site de rails à l'aide de SQLite3.
Environ une fois toutes les 500 demandes environ, je reçois un
ActiveRecord :: StatementInvalid (SQLite3 :: BusyException: la base de données est verrouillée: ...
Quel est le moyen de résoudre ce problème qui serait peu envahissant pour mon code?
J'utilise actuellement SQLLite parce que vous pouvez stocker la base de données dans le contrôle de code source, ce qui rend la sauvegarde naturelle et vous permet de transmettre les modifications très rapidement. Cependant, ce n'est évidemment pas vraiment configuré pour un accès simultané. Je migrerai vers MySQL demain matin.
La solution
Par défaut, sqlite renvoie immédiatement avec une erreur bloquée, occupé si la base de données est occupée et verrouillée. Vous pouvez lui demander d'attendre et de continuer à essayer pendant un moment avant d'abandonner. Cela résout généralement le problème, sauf si vous avez 1 000 threads accédant à votre base de données, lorsque je conviens que SQLite serait inapproprié.
// set SQLite to wait and retry for up to 100ms if database locked sqlite3_busy_timeout( db, 100 );
Autres conseils
Vous avez mentionné qu'il s'agissait d'un site Rails. Rails vous permet de définir le délai avant nouvelle tentative SQLite dans votre fichier de configuration database.yml:
production:
adapter: sqlite3
database: db/mysite_prod.sqlite3
timeout: 10000
La valeur de délai d'attente est spécifiée en millisecondes. Si vous augmentez cette valeur à 10 ou 15 secondes, le nombre d'exceptions BusyExceptions que vous voyez dans votre journal devrait diminuer.
Ceci n’est cependant qu’une solution temporaire. Si votre site nécessite une concurrence réelle, vous devrez migrer vers un autre moteur de base de données.
Toutes ces choses sont vraies, mais cela ne répond pas à la question suivante: pourquoi mon application Rails pose-t-elle parfois une exception SQLite3 :: BusyException en production?
@Shalmanese: à quoi ressemble l'environnement d'hébergement de production? Est-ce sur un hôte partagé? Le répertoire contenant la base de données sqlite est-il sur un partage NFS? (Probablement sur un hôte partagé).
Ce problème est probablement lié au phénomène de verrouillage de fichiers avec des partages NFS et au manque de simultanéité de SQLite.
Juste pour le compte rendu. Dans une application avec Rails 2.3.8, nous avons découvert que Rails ignorait le "timeout". option proposée par Rifkin Habsburg.
Après des recherches plus approfondies, nous avons découvert un bug éventuellement lié à Rails dev: http: //dev.rubyonrails. org / ticket / 8811 . Et après quelques recherches supplémentaires, nous avons trouvé la solution (testée avec Rails 2.3.8):
Éditez ce fichier ActiveRecord: activerecord-2.3.8 / lib / active_record / connection_adapters / sqlite_adapter.rb
Remplacez ceci:
def begin_db_transaction #:nodoc:
catch_schema_changes { @connection.transaction }
end
avec
def begin_db_transaction #:nodoc:
catch_schema_changes { @connection.transaction(:immediate) }
end
Et c'est tout! Nous n'avons pas remarqué de baisse de performances et maintenant, l'application prend en charge de nombreuses autres pétitions sans interruption (elle attend la fin du délai). Sqlite est sympa!
bundle exec rake db:reset
Cela a fonctionné pour moi, il va réinitialiser et afficher la migration en attente.
Source: ce lien
- Open the database
db = sqlite3.open("filename")
-- Ten attempts are made to proceed, if the database is locked
function my_busy_handler(attempts_made)
if attempts_made < 10 then
return true
else
return false
end
end
-- Set the new busy handler
db:set_busy_handler(my_busy_handler)
-- Use the database
db:exec(...)
SQLite peut permettre à d’autres processus d’attendre la fin du processus en cours.
J'utilise cette ligne pour établir une connexion lorsque je sais que plusieurs processus tentent d'accéder à la base de données SQL:
conn = sqlite3.connect ('nomfichier', isolation_level = 'exclusif' )
Selon la documentation de Python Sqlite:
Vous pouvez contrôler quel type de début déclarations pysqlite implicitement exécute (ou pas du tout) via le paramètre isolation_level au connect () appel, ou via le propriété isolation_level de connexions.
J'ai eu un problème similaire avec rake db: migrate. Le problème était que le répertoire de travail se trouvait sur un partage SMB. Je l'ai corrigé en copiant le dossier sur ma machine locale.
Si vous rencontrez ce problème mais que l'augmentation du délai d'attente ne change rien , il est possible que vous rencontriez un autre problème de simultanéité avec les transactions. Voici le résumé:
- Commencer une transaction (acquiert un verrou SHARED )
- Lire certaines données de la base de données (nous utilisons toujours le verrou SHARED )
- Pendant ce temps, un autre processus démarre une transaction et écrit des données (acquisition du verrou RESERVED ).
- Ensuite, vous essayez d'écrire, vous essayez maintenant de demander le verrou RESERVED
- SQLite lève l'exception SQLITE_BUSY immédiatement (indépendamment de votre délai d'expiration), car vos lectures précédentes risquent de ne plus être exactes au moment où vous pouvez obtenir le verrou
RÉSERVÉ .
Une solution à ce problème consiste à appliquer une correction à l'adaptateur sqlite active_record
afin d'obtenir un verrou RESERVED directement au début de la transaction en complétant le code : immédiat
option au pilote. Cela réduira un peu les performances, mais au moins toutes vos transactions respecteront votre délai d'attente et se produiront l'une après l'autre. Voici comment faire cela en utilisant prepend
(Ruby 2.0+) et le placer dans un initialiseur:
module SqliteTransactionFix
def begin_db_transaction
log('begin immediate transaction', nil) { @connection.transaction(:immediate) }
end
end
module ActiveRecord
module ConnectionAdapters
class SQLiteAdapter < AbstractAdapter
prepend SqliteTransactionFix
end
end
end
Lisez la suite ici: https://rails.lighthouseapp.com/projects/8994/tickets/5941-sqlite3busyexceptions-are-raised-immediately-in-some-cases-despite-setting-sqlite3_busy_timeout
La plupart des réponses concernent les rails plutôt que les rubis bruts, et les questions OP sur les rails, ce qui est correct. :)
Je souhaite simplement laisser cette solution ici si un utilisateur de ruby ??brut rencontre ce problème et n'utilise pas de configuration yml.
Une fois la connexion établie, vous pouvez la définir comme suit:
db = SQLite3::Database.new "#{path_to_your_db}/your_file.db"
db.busy_timeout=(15000) # in ms, meaning it will retry for 15 seconds before it raises an exception.
#This can be any number you want. Default value is 0.
À quelle table accède-t-on lorsque le verrou est rencontré?
Avez-vous des transactions de longue durée?
Pouvez-vous déterminer quelles demandes étaient encore en cours de traitement lorsque le verrou a été rencontré?
Argh - le fléau de mon existence au cours de la semaine dernière. Sqlite3 verrouille le fichier db quand un processus écrit dans la base de données. IE toute requête de type UPDATE / INSERT (également sélectionner count (*) pour une raison quelconque). Cependant, il gère très bien plusieurs lectures.
Ainsi, je suis finalement suffisamment frustré pour écrire mon propre code de verrouillage de threads autour des appels de base de données. En veillant à ce que l'application ne puisse avoir qu'un seul thread écrivant dans la base de données à tout moment, j'ai été en mesure de faire évoluer 1000 threads.
Et oui, c'est lent comme l'enfer. Mais c’est aussi assez rapide et correct , ce qui est une belle propriété à avoir.
J'ai trouvé un blocage sur l'extension sqlite3 ruby ??et je le répare ici: essayez-le et voyez si cela résout votre problème.
https://github.com/dxj19831029/sqlite3-ruby
J'ai ouvert une demande d'extraction, plus aucune réponse d'eux.
Quoi qu'il en soit, une exception occupée est attendue, comme décrit dans sqlite3.
Soyez conscient de cette condition: sqlite occupé
The presence of a busy handler does not guarantee that it will be invoked when there is lock contention. If SQLite determines that invoking the busy handler could result in a deadlock, it will go ahead and return SQLITE_BUSY or SQLITE_IOERR_BLOCKED instead of invoking the busy handler. Consider a scenario where one process is holding a read lock that it is trying to promote to a reserved lock and a second process is holding a reserved lock that it is trying to promote to an exclusive lock. The first process cannot proceed because it is blocked by the second and the second process cannot proceed because it is blocked by the first. If both processes invoke the busy handlers, neither will make any progress. Therefore, SQLite returns SQLITE_BUSY for the first process, hoping that this will induce the first process to release its read lock and allow the second process to proceed.
Si vous remplissez cette condition, le délai d'attente n'est plus valide. Pour l'éviter, ne placez pas select dans begin / commit. ou utilisez le verrou exclusif pour begin / commit.
J'espère que ça aide. :)
il s'agit souvent d'une erreur consécutive de plusieurs processus accédant à la même base de données, c'est-à-dire si l'option "Autoriser une seule instance" " le drapeau n'a pas été défini dans RubyMine
Essayez d’exécuter ce qui suit, cela pourrait aider:
ActiveRecord::Base.connection.execute("BEGIN TRANSACTION; END;")
De: Ruby: SQLite3 :: BusyException: la base de données est verrouillée:
Ceci peut effacer n'importe quelle transaction bloquant le système
Je pense que cela se produit lorsqu'une transaction arrive à expiration. Vous devriez vraiment utiliser un " réel " base de données. Quelque chose comme Drizzle ou MySQL. Pourquoi préférez-vous SQLite aux deux options précédentes?