New: Retry SQLite writes for database is locked errors

(cherry picked from commit 2e1289b9248a70ce50bde52a66d3a589f3dcb8f5)
This commit is contained in:
Bogdan 2025-09-09 17:16:23 +03:00 committed by bakerboy448
parent e63691935d
commit cf465899b4

View file

@ -1,13 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Data.SQLite;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using Dapper; using Dapper;
using NLog;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using Polly;
using Polly.Retry;
namespace NzbDrone.Core.Datastore namespace NzbDrone.Core.Datastore
{ {
@ -40,12 +45,31 @@ public interface IBasicRepository<TModel>
public class BasicRepository<TModel> : IBasicRepository<TModel> public class BasicRepository<TModel> : IBasicRepository<TModel>
where TModel : ModelBase, new() where TModel : ModelBase, new()
{ {
private static readonly ILogger Logger = NzbDroneLogger.GetLogger(typeof(BasicRepository<TModel>));
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly PropertyInfo _keyProperty; private readonly PropertyInfo _keyProperty;
private readonly List<PropertyInfo> _properties; private readonly List<PropertyInfo> _properties;
private readonly string _updateSql; private readonly string _updateSql;
private readonly string _insertSql; private readonly string _insertSql;
private static ResiliencePipeline RetryStrategy => new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<SQLiteException>(ex => ex.ResultCode == SQLiteErrorCode.Busy),
Delay = TimeSpan.FromMilliseconds(100),
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
OnRetry = args =>
{
Logger.Warn(args.Outcome.Exception, "Failed writing to database. Retry #{0}", args.AttemptNumber);
return default;
}
})
.Build();
protected readonly IDatabase _database; protected readonly IDatabase _database;
protected readonly string _table; protected readonly string _table;
@ -186,7 +210,9 @@ private string GetInsertSql()
private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model) private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model)
{ {
SqlBuilderExtensions.LogQuery(_insertSql, model); SqlBuilderExtensions.LogQuery(_insertSql, model);
var multi = connection.QueryMultiple(_insertSql, model, transaction);
var multi = RetryStrategy.Execute(static (state, _) => state.connection.QueryMultiple(state._insertSql, state.model, state.transaction), (connection, _insertSql, model, transaction));
var multiRead = multi.Read(); var multiRead = multi.Read();
var id = (int)(multiRead.First().id ?? multiRead.First().Id); var id = (int)(multiRead.First().id ?? multiRead.First().Id);
_keyProperty.SetValue(model, id); _keyProperty.SetValue(model, id);
@ -381,7 +407,7 @@ private void UpdateFields(IDbConnection connection, IDbTransaction transaction,
SqlBuilderExtensions.LogQuery(sql, model); SqlBuilderExtensions.LogQuery(sql, model);
connection.Execute(sql, model, transaction: transaction); RetryStrategy.Execute(static (state, _) => state.connection.Execute(state.sql, state.model, transaction: state.transaction), (connection, sql, model, transaction));
} }
private void UpdateFields(IDbConnection connection, IDbTransaction transaction, IList<TModel> models, List<PropertyInfo> propertiesToUpdate) private void UpdateFields(IDbConnection connection, IDbTransaction transaction, IList<TModel> models, List<PropertyInfo> propertiesToUpdate)
@ -393,7 +419,7 @@ private void UpdateFields(IDbConnection connection, IDbTransaction transaction,
SqlBuilderExtensions.LogQuery(sql, model); SqlBuilderExtensions.LogQuery(sql, model);
} }
connection.Execute(sql, models, transaction: transaction); RetryStrategy.Execute(static (state, _) => state.connection.Execute(state.sql, state.models, transaction: state.transaction), (connection, sql, models, transaction));
} }
protected virtual SqlBuilder PagedBuilder() => Builder(); protected virtual SqlBuilder PagedBuilder() => Builder();