mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-26 21:02:28 +02:00
fix(db): prevent concurrent startup locks
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:spotiflac_android/services/sqlite_helpers.dart' as sqlite;
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
final _log = AppLogger('AppStateDb');
|
||||
@@ -25,31 +24,20 @@ const _recentMigrationKey = 'app_state_migrated_recent_to_sqlite_v1';
|
||||
|
||||
class AppStateDatabase {
|
||||
static final AppStateDatabase instance = AppStateDatabase._init();
|
||||
static Database? _database;
|
||||
static final sqlite.SingleFlightInitializer<Database> _database =
|
||||
sqlite.SingleFlightInitializer<Database>();
|
||||
|
||||
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
|
||||
AppStateDatabase._init();
|
||||
|
||||
Future<Database> get database async {
|
||||
if (_database != null) return _database!;
|
||||
_database = await _initDb();
|
||||
return _database!;
|
||||
}
|
||||
Future<Database> get database => _database.getOrCreate(_initDb);
|
||||
|
||||
Future<Database> _initDb() async {
|
||||
final dbPath = await getApplicationDocumentsDirectory();
|
||||
final path = join(dbPath.path, _dbFileName);
|
||||
|
||||
_log.i('Initializing app state database at: $path');
|
||||
|
||||
return openDatabase(
|
||||
path,
|
||||
Future<Database> _initDb() {
|
||||
return sqlite.openAppDatabase(
|
||||
_dbFileName,
|
||||
version: _dbVersion,
|
||||
onConfigure: (db) async {
|
||||
await db.rawQuery('PRAGMA journal_mode = WAL');
|
||||
await db.execute('PRAGMA synchronous = NORMAL');
|
||||
},
|
||||
incrementalAutoVacuum: false,
|
||||
onCreate: _createDb,
|
||||
onUpgrade: _upgradeDb,
|
||||
);
|
||||
|
||||
@@ -67,19 +67,20 @@ class HistoryBatchLookupRequest {
|
||||
class HistoryDatabase {
|
||||
static const int schemaVersion = 10;
|
||||
static final HistoryDatabase instance = HistoryDatabase._init();
|
||||
static Database? _database;
|
||||
static final sqlite.SingleFlightInitializer<Database> _database =
|
||||
sqlite.SingleFlightInitializer<Database>();
|
||||
|
||||
HistoryDatabase._init();
|
||||
|
||||
Future<Database> get database async {
|
||||
if (_database != null) return _database!;
|
||||
_database = await sqlite.openAppDatabase(
|
||||
'history.db',
|
||||
version: schemaVersion,
|
||||
onCreate: _createDB,
|
||||
onUpgrade: _upgradeDB,
|
||||
Future<Database> get database {
|
||||
return _database.getOrCreate(
|
||||
() => sqlite.openAppDatabase(
|
||||
'history.db',
|
||||
version: schemaVersion,
|
||||
onCreate: _createDB,
|
||||
onUpgrade: _upgradeDB,
|
||||
),
|
||||
);
|
||||
return _database!;
|
||||
}
|
||||
|
||||
Future<void> _createDB(Database db, int version) async {
|
||||
@@ -895,7 +896,7 @@ class HistoryDatabase {
|
||||
Future<void> close() async {
|
||||
final db = await database;
|
||||
await db.close();
|
||||
_database = null;
|
||||
_database.reset();
|
||||
}
|
||||
|
||||
Future<void> updateFilePath(
|
||||
|
||||
@@ -60,22 +60,23 @@ class PlaylistPickerSummaryRow {
|
||||
class LibraryCollectionsDatabase {
|
||||
static final LibraryCollectionsDatabase instance =
|
||||
LibraryCollectionsDatabase._init();
|
||||
static Database? _database;
|
||||
static final sqlite.SingleFlightInitializer<Database> _database =
|
||||
sqlite.SingleFlightInitializer<Database>();
|
||||
|
||||
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
|
||||
LibraryCollectionsDatabase._init();
|
||||
|
||||
Future<Database> get database async {
|
||||
if (_database != null) return _database!;
|
||||
_database = await sqlite.openAppDatabase(
|
||||
_dbFileName,
|
||||
version: _dbVersion,
|
||||
foreignKeys: true,
|
||||
onCreate: _createDb,
|
||||
onUpgrade: _upgradeDb,
|
||||
Future<Database> get database {
|
||||
return _database.getOrCreate(
|
||||
() => sqlite.openAppDatabase(
|
||||
_dbFileName,
|
||||
version: _dbVersion,
|
||||
foreignKeys: true,
|
||||
onCreate: _createDb,
|
||||
onUpgrade: _upgradeDb,
|
||||
),
|
||||
);
|
||||
return _database!;
|
||||
}
|
||||
|
||||
Future<void> _createDb(Database db, int version) async {
|
||||
|
||||
@@ -18,20 +18,21 @@ class LibraryDatabase {
|
||||
static final LibraryDatabase instance = LibraryDatabase._init();
|
||||
static const int schemaVersion = 9;
|
||||
static const int audioMetadataScanVersion = 1;
|
||||
static Database? _database;
|
||||
static final sqlite.SingleFlightInitializer<Database> _database =
|
||||
sqlite.SingleFlightInitializer<Database>();
|
||||
bool _historyAttached = false;
|
||||
|
||||
LibraryDatabase._init();
|
||||
|
||||
Future<Database> get database async {
|
||||
if (_database != null) return _database!;
|
||||
_database = await sqlite.openAppDatabase(
|
||||
'local_library.db',
|
||||
version: schemaVersion,
|
||||
onCreate: _createDB,
|
||||
onUpgrade: _upgradeDB,
|
||||
Future<Database> get database {
|
||||
return _database.getOrCreate(
|
||||
() => sqlite.openAppDatabase(
|
||||
'local_library.db',
|
||||
version: schemaVersion,
|
||||
onCreate: _createDB,
|
||||
onUpgrade: _upgradeDB,
|
||||
),
|
||||
);
|
||||
return _database!;
|
||||
}
|
||||
|
||||
Future<void> _ensureHistoryAttached(Database db) async {
|
||||
@@ -1013,7 +1014,7 @@ class LibraryDatabase {
|
||||
Future<void> close() async {
|
||||
final db = await database;
|
||||
await db.close();
|
||||
_database = null;
|
||||
_database.reset();
|
||||
_historyAttached = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,39 @@ import 'package:sqflite/sqflite.dart';
|
||||
|
||||
final _log = AppLogger('AppSqlite');
|
||||
|
||||
/// Caches an asynchronously-created value while also coalescing concurrent
|
||||
/// callers onto the same in-flight initialization.
|
||||
class SingleFlightInitializer<T extends Object> {
|
||||
T? _value;
|
||||
Future<T>? _initializing;
|
||||
|
||||
Future<T> getOrCreate(Future<T> Function() create) {
|
||||
final value = _value;
|
||||
if (value != null) return Future<T>.value(value);
|
||||
|
||||
final initializing = _initializing;
|
||||
if (initializing != null) return initializing;
|
||||
|
||||
late final Future<T> future;
|
||||
future = Future<T>.sync(create)
|
||||
.then((value) {
|
||||
_value = value;
|
||||
return value;
|
||||
})
|
||||
.whenComplete(() {
|
||||
if (identical(_initializing, future)) {
|
||||
_initializing = null;
|
||||
}
|
||||
});
|
||||
_initializing = future;
|
||||
return future;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_value = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a database file in the app documents directory with the shared
|
||||
/// WAL + synchronous=NORMAL configuration.
|
||||
Future<Database> openAppDatabase(
|
||||
@@ -15,6 +48,7 @@ Future<Database> openAppDatabase(
|
||||
required Future<void> Function(Database db, int oldVersion, int newVersion)
|
||||
onUpgrade,
|
||||
bool foreignKeys = false,
|
||||
bool incrementalAutoVacuum = true,
|
||||
}) async {
|
||||
final dbPath = await getApplicationDocumentsDirectory();
|
||||
final path = join(dbPath.path, fileName);
|
||||
@@ -25,13 +59,28 @@ Future<Database> openAppDatabase(
|
||||
path,
|
||||
version: version,
|
||||
onConfigure: (db) async {
|
||||
// Set this before any other PRAGMA so transient writer contention waits
|
||||
// instead of immediately surfacing SQLITE_BUSY during startup.
|
||||
await db.rawQuery('PRAGMA busy_timeout = 5000');
|
||||
if (foreignKeys) {
|
||||
await db.execute('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
// Without auto_vacuum the file never shrinks after deletes — its size
|
||||
// is a permanent high-water mark. Only takes effect on newly created
|
||||
// databases; existing files keep their mode until a manual VACUUM.
|
||||
await db.execute('PRAGMA auto_vacuum = INCREMENTAL');
|
||||
if (incrementalAutoVacuum) {
|
||||
final tables = await db.rawQuery('''
|
||||
SELECT 1
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name NOT LIKE 'sqlite_%'
|
||||
AND name != 'android_metadata'
|
||||
LIMIT 1
|
||||
''');
|
||||
// Changing from NONE must happen outside a transaction and before the
|
||||
// first application table is created. Android may already have added
|
||||
// its internal android_metadata table at this point.
|
||||
if (tables.isEmpty) {
|
||||
await db.execute('PRAGMA auto_vacuum = INCREMENTAL');
|
||||
}
|
||||
}
|
||||
await db.rawQuery('PRAGMA journal_mode = WAL');
|
||||
await db.execute('PRAGMA synchronous = NORMAL');
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user