Skip to content

Commit

Permalink
Fixes #236 - Restore not working corectly
Browse files Browse the repository at this point in the history
came up with a solution to forcefully release the lock on the sqlite database file.
ensured all contexts are being disposed properly.
  • Loading branch information
replaysMike committed Apr 23, 2024
1 parent 3a6e691 commit 8c2d7d7
Show file tree
Hide file tree
Showing 13 changed files with 94 additions and 31 deletions.
2 changes: 1 addition & 1 deletion Binner/Library/Binner.Common/IO/BackupProvider.MySql.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ private async Task RestoreMySqlAsync(DbInfo dbInfo)
};

// drop the database if it exists
var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
await using var conn = context.Database.GetDbConnection();
await using var dropCmd = conn.CreateCommand();
await conn.OpenAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private async Task RestorePostgresqlAsync(DbInfo dbInfo)
};

// drop the database if it exists
var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
await using var conn = context.Database.GetDbConnection();
await conn.OpenAsync();
await using var cmdDrop = conn.CreateCommand();
Expand Down
4 changes: 2 additions & 2 deletions Binner/Library/Binner.Common/IO/BackupProvider.SqlServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ private async Task<MemoryStream> BackupSqlServerAsync()
};
var dbName = builder["Database"];

var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
await using var conn = context.Database.GetDbConnection();
await using var cmd = conn.CreateCommand();
cmd.CommandText = $"BACKUP DATABASE [{dbName}] TO DISK = '{filename}' WITH FORMAT";
Expand Down Expand Up @@ -54,7 +54,7 @@ private async Task RestoreSqlServerAsync(DbInfo dbInfo)
};
var dbName = builder["Database"];

var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
await using var conn = context.Database.GetDbConnection();
await using var cmd = conn.CreateCommand();
cmd.CommandText = $@"USE [master];
Expand Down
62 changes: 58 additions & 4 deletions Binner/Library/Binner.Common/IO/BackupProvider.Sqlite.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
using System.IO;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Binner.Common.IO
Expand All @@ -21,9 +26,58 @@ private async Task<MemoryStream> BackupSqliteAsync()
private async Task RestoreSqliteAsync(DbInfo dbInfo)
{
var filename = _configuration.ProviderConfiguration["Filename"] ?? throw new BinnerConfigurationException("Error: no Filename specified in StorageProviderConfiguration");
await using var fileRef = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
dbInfo!.Database!.Position = 0;
await dbInfo.Database.CopyToAsync(fileRef);

// get a connection so we can use the handle to forcefully close the Sqlite database
await using var context = await _contextFactory.CreateDbContextAsync();
var conn = context.Database.GetDbConnection() as SqliteConnection;
conn.Open();
// forcefully close the sqlite database using it's handle
var result = SQLitePCL.raw.sqlite3_close_v2(conn.Handle);
conn.Handle.Close();
conn.Handle.Dispose();

// required GC collection
try
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
catch (Exception ex)
{
_logger.LogError($"Failed to garbage collect while closing existing Sqlite database at '{filename}' for restore operation.");
}

var deleteSuccess = false;
var attempts = 1;
while ((attempts < 10) && (!deleteSuccess))
{
try
{
Thread.Sleep(attempts * 100);
File.Delete(filename);
_logger.LogInformation($"Deleted existing Sqlite database at '{filename}' for restore operation.");
deleteSuccess = true;
}
catch (IOException e) // delete only throws this on locking
{
_logger.LogError($"Failed to delete existing Sqlite database at '{filename}' for restore operation. Retrying {attempts} of 10...");
attempts++;
}
}

if (deleteSuccess)
{
await using var fileRef = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
//dbInfo!.Database!.Position = 0;
dbInfo.Database.Seek(0, SeekOrigin.Begin);
await dbInfo.Database.CopyToAsync(fileRef);
_logger.LogInformation($"Restored Sqlite database at '{filename}'.");
}
else
{
_logger.LogError($"Failed to overwrite existing Sqlite database at '{filename}' for restore operation.");
throw new InvalidOperationException($"Unable to overwrite the current Sqlite database '{filename}'!");
}
}
}
}
13 changes: 11 additions & 2 deletions Binner/Library/Binner.Common/IO/BackupProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ public async Task RestoreAsync(UploadFile backupFile)
{
case "sqlite":
case "binner":
await RestoreSqliteAsync(dbInfo);
try
{
await RestoreSqliteAsync(dbInfo);
}
catch (ObjectDisposedException)
{
// expected, unavoidable
}
await ProcessFileOperationsAsync(dbInfo);
return;
case "sqlserver":
Expand Down Expand Up @@ -261,7 +268,7 @@ public class DbInfo
{
public BackupInfo? BackupInfo { get; set; }
public MemoryStream? Database { get; set; }
public List<FileOperation> FileOperations { get; set; } = new ();
public List<FileOperation> FileOperations { get; set; } = new();
}

public class FileOperation
Expand All @@ -275,5 +282,7 @@ public FileOperation(string destinationFilename, byte[] data)
DestinationFilename = destinationFilename;
Data = data;
}

public override string ToString() => DestinationFilename;
}
}
6 changes: 3 additions & 3 deletions Binner/Library/Binner.Common/Services/AccountService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public AccountService(IDbContextFactory<BinnerContext> contextFactory, IMapper m
public async Task<Account> GetAccountAsync()
{
var userContext = _requestContext.GetUserContext();
var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var entity = await GetAccountQueryable(context)
.Where(x => x.UserId == userContext.UserId && x.OrganizationId == userContext.OrganizationId)
.AsSplitQuery()
Expand All @@ -60,7 +60,7 @@ public async Task<Account> GetAccountAsync()
public async Task<UpdateAccountResponse> UpdateAccountAsync(Account account)
{
var userContext = _requestContext.GetUserContext();
var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var entity = await GetAccountQueryable(context)
.Where(x => x.UserId == userContext.UserId)
.AsSplitQuery()
Expand Down Expand Up @@ -114,7 +114,7 @@ public async Task<UpdateAccountResponse> UpdateAccountAsync(Account account)
public async Task UploadProfileImageAsync(MemoryStream stream, string originalFilename, string contentType, long length)
{
var userContext = _requestContext.GetUserContext();
var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var entity = await GetAccountQueryable(context)
.Where(x => x.UserId == userContext.UserId && x.OrganizationId == userContext.OrganizationId)
.AsSplitQuery()
Expand Down
2 changes: 1 addition & 1 deletion Binner/Library/Binner.Common/Services/AdminService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async Task<SystemInfoResponse> GetSystemInfoAsync()
{
var model = new SystemInfoResponse();
var userContext = _requestContext.GetUserContext();
var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
Assembly? entryAssembly = null;
try
{
Expand Down
12 changes: 6 additions & 6 deletions Binner/Library/Binner.Common/Services/AuthenticationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public async Task<AuthenticationResponse> AuthenticateAsync(AuthenticationReques
{
if (model == null) throw new ArgumentNullException(nameof(model));

using var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
using var transaction = await context.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
try
{
Expand Down Expand Up @@ -154,7 +154,7 @@ private async Task<AuthenticationResponse> CreateAuthenticationLoginAsync(Binner
public async Task<AuthenticationResponse> RefreshTokenAsync(string token)
{
if (string.IsNullOrEmpty(token)) throw new ArgumentNullException(nameof(token));
using var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
// todo: seems to be causing deadlocks, need to investigate
//using var transaction = await context.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);

Expand Down Expand Up @@ -254,7 +254,7 @@ public async Task RevokeTokenAsync(string token)
public async Task<UserContext?> GetUserAsync(int userId)
{
if (userId == 0) throw new ArgumentNullException(nameof(userId));
using var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var userContext = await context.Users.FirstOrDefaultAsync(x => x.UserId == userId);
return userContext != null ? Map(userContext) : null;
}
Expand Down Expand Up @@ -351,7 +351,7 @@ public async Task<PasswordRecoveryResponse> SendPasswordResetRequest(PasswordRec
{
if (request == null) throw new ArgumentNullException(nameof(request));

using var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var user = await context.Users
.FirstOrDefaultAsync(x => x.EmailAddress == request.EmailAddress);
if (user == null)
Expand Down Expand Up @@ -395,7 +395,7 @@ public async Task<PasswordRecoveryResponse> SendPasswordResetRequest(PasswordRec
public async Task<PasswordRecoveryResponse> ValidatePasswordResetTokenAsync(ConfirmPasswordRecoveryRequest request)
{
if (request == null) throw new ArgumentNullException(nameof(request));
using var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var user = await context.Users
.FirstOrDefaultAsync(x => x.EmailAddress == request.EmailAddress);
if (user == null)
Expand Down Expand Up @@ -435,7 +435,7 @@ public async Task<PasswordRecoveryResponse> ValidatePasswordResetTokenAsync(Conf
public async Task<AuthenticationResponse> ResetPasswordUsingTokenAsync(PasswordRecoverySetNewPasswordRequest request)
{
if (request == null) throw new ArgumentNullException(nameof(request));
using var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var user = await context.Users
.FirstOrDefaultAsync(x => x.EmailAddress == request.EmailAddress);
if (user == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public async Task<T> CreateAsync<T>(int userId)
#pragma warning restore CS1998
{
// create a db context
//using var context = await _contextFactory.CreateDbContextAsync();
//await using var context = await _contextFactory.CreateDbContextAsync();
/*var userIntegrationConfiguration = await context.UserIntegrationConfigurations
.Where(x => x.UserId.Equals(userId))
.FirstOrDefaultAsync()
Expand Down Expand Up @@ -221,7 +221,7 @@ public async Task<IIntegrationApi> CreateAsync(Type apiType, int userId)
#pragma warning restore CS1998
{
// create a db context
//using var context = await _contextFactory.CreateDbContextAsync();
//await using var context = await _contextFactory.CreateDbContextAsync();
/*var userIntegrationConfiguration = await context.UserIntegrationConfigurations
.Where(x => x.UserId.Equals(userId))
.FirstOrDefaultAsync()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public async Task<TestApiResponse> TestApiAsync(TestApiRequest request)
#pragma warning restore CS1998
{
// create a db context
//using var context = await _contextFactory.CreateDbContextAsync();
//await using var context = await _contextFactory.CreateDbContextAsync();
/*var userIntegrationConfiguration = await context.UserIntegrationConfigurations
.Where(x => x.UserId.Equals(userId))
.FirstOrDefaultAsync()
Expand Down
2 changes: 1 addition & 1 deletion Binner/Library/Binner.Common/Services/PrintService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public async Task<bool> DeleteLabelTemplateAsync(LabelTemplate model)
public async Task<ICollection<LabelTemplate>> GetLabelTemplatesAsync()
{
var user = _requestContext.GetUserContext();
var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var entities = await context.LabelTemplates
.Where(x => x.OrganizationId == user.OrganizationId)
.ToListAsync();
Expand Down
10 changes: 5 additions & 5 deletions Binner/Library/Binner.Common/Services/ProjectService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ public async Task<ICollection<ProjectProduceHistory>> GetProduceHistoryAsync(Get
var user = _requestContext.GetUserContext();
if (user == null) throw new ArgumentNullException(nameof(user));

var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var pageRecords = (request.Page - 1) * request.Results;
var entities = await context.ProjectProduceHistory
.Include(x => x.ProjectPcbProduceHistory)
Expand Down Expand Up @@ -347,7 +347,7 @@ public async Task<ProjectProduceHistory> UpdateProduceHistoryAsync(ProjectProduc
var user = _requestContext.GetUserContext();
if (user == null) throw new ArgumentNullException(nameof(user));

var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();

var entity = await context.ProjectProduceHistory
.Include(x => x.ProjectPcbProduceHistory)
Expand Down Expand Up @@ -379,7 +379,7 @@ public async Task<bool> DeleteProduceHistoryAsync(ProjectProduceHistory request)
var user = _requestContext.GetUserContext();
if (user == null) throw new ArgumentNullException(nameof(user));

var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var entity = await context.ProjectProduceHistory
.Include(x => x.Project)
.ThenInclude(x => x.ProjectPartAssignments)
Expand All @@ -406,7 +406,7 @@ public async Task<bool> DeletePcbProduceHistoryAsync(ProjectPcbProduceHistory re
var user = _requestContext.GetUserContext();
if (user == null) throw new ArgumentNullException(nameof(user));

var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var entity = await context.ProjectPcbProduceHistory
.Include(x => x.Pcb)
.Include(x => x.ProjectProduceHistory)
Expand Down Expand Up @@ -478,7 +478,7 @@ public async Task<bool> ProducePcbAsync(ProduceBomPcbRequest request)
{
// get all the parts in the project
var user = _requestContext.GetUserContext();
var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var numberOfPcbsProduced = request.Quantity;

var project = await GetProjectAsync(request.ProjectId);
Expand Down
4 changes: 2 additions & 2 deletions Binner/Library/Binner.Common/Services/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public async Task<bool> DeleteUserAsync(int userId)
if (userId == 1)
throw new SecurityException($"The root admin user cannot be deleted.");
var userContext = _requestContext.GetUserContext();
var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var entity = await GetUserQueryable(context, userContext)
.Where(x => x.UserId == userId)
.AsSplitQuery()
Expand All @@ -83,7 +83,7 @@ public async Task<bool> DeleteUserAsync(int userId)

public async Task<IUserContext?> ValidateUserImageToken(string token)
{
var context = await _contextFactory.CreateDbContextAsync();
await using var context = await _contextFactory.CreateDbContextAsync();
var userToken = await context.UserTokens
.Include(x => x.User)
.FirstOrDefaultAsync(x =>
Expand Down

0 comments on commit 8c2d7d7

Please sign in to comment.