From 9cceb1b4a01cfb38f3f58a09bdc9da25896bc3e5 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Wed, 18 Jan 2023 03:06:23 +0100 Subject: [PATCH] support `readOnly` backend in LC client/data dbs (#4518) When accessing DB in `readOnly` mode that does not already have latest schema, initial writes trigger `attempt to write a readonly database`. Avoid that by only writing schema when DB is not `readOnly`, and provide data from legacy tables if such are present. --- beacon_chain/beacon_chain_db_light_client.nim | 95 ++++++++++++++----- beacon_chain/light_client_db.nim | 62 +++++++++--- 2 files changed, 121 insertions(+), 36 deletions(-) diff --git a/beacon_chain/beacon_chain_db_light_client.nim b/beacon_chain/beacon_chain_db_light_client.nim index 21881ef8f..938e50b45 100644 --- a/beacon_chain/beacon_chain_db_light_client.nim +++ b/beacon_chain/beacon_chain_db_light_client.nim @@ -52,6 +52,7 @@ type keepFromStmt: SqliteStmt[int64, void] LegacyBestLightClientUpdateStore = object + getStmt: SqliteStmt[int64, (int64, seq[byte])] putStmt: SqliteStmt[(int64, seq[byte]), void] delStmt: SqliteStmt[int64, void] delFromStmt: SqliteStmt[int64, void] @@ -94,9 +95,17 @@ type ## Tracks the finalized sync committee periods for which complete data ## has been imported (from `dag.tail.slot`). +template disposeSafe(s: untyped): untyped = + if distinctBase(s) != nil: + s.dispose() + s = nil + proc initCurrentBranchesStore( backend: SqStoreRef, name: string): KvResult[CurrentSyncCommitteeBranchStore] = + if backend.readOnly and not ? backend.hasTable(name): + return ok CurrentSyncCommitteeBranchStore() + ? backend.exec(""" CREATE TABLE IF NOT EXISTS `""" & name & """` ( `slot` INTEGER PRIMARY KEY, -- `Slot` (up through 2^63-1) @@ -131,15 +140,16 @@ proc initCurrentBranchesStore( putStmt: putStmt, keepFromStmt: keepFromStmt) -func close(store: CurrentSyncCommitteeBranchStore) = - store.containsStmt.dispose() - store.getStmt.dispose() - store.putStmt.dispose() - store.keepFromStmt.dispose() +func close(store: var CurrentSyncCommitteeBranchStore) = + store.containsStmt.disposeSafe() + store.getStmt.disposeSafe() + store.putStmt.disposeSafe() + store.keepFromStmt.disposeSafe() func hasCurrentSyncCommitteeBranch*( db: LightClientDataDB, slot: Slot): bool = - if not slot.isSupportedBySQLite: + if not slot.isSupportedBySQLite or + distinctBase(db.currentBranches.containsStmt) == nil: return false var exists: int64 for res in db.currentBranches.containsStmt.exec(slot.int64, exists): @@ -150,7 +160,8 @@ func hasCurrentSyncCommitteeBranch*( proc getCurrentSyncCommitteeBranch*( db: LightClientDataDB, slot: Slot): altair.CurrentSyncCommitteeBranch = - if not slot.isSupportedBySQLite: + if not slot.isSupportedBySQLite or + distinctBase(db.currentBranches.getStmt) == nil: return default(altair.CurrentSyncCommitteeBranch) var branch: seq[byte] for res in db.currentBranches.getStmt.exec(slot.int64, branch): @@ -165,6 +176,7 @@ proc getCurrentSyncCommitteeBranch*( func putCurrentSyncCommitteeBranch*( db: LightClientDataDB, slot: Slot, branch: altair.CurrentSyncCommitteeBranch) = + doAssert not db.backend.readOnly # All `stmt` are non-nil if not slot.isSupportedBySQLite: return let res = db.currentBranches.putStmt.exec((slot.int64, SSZ.encode(branch))) @@ -174,6 +186,9 @@ proc initLegacyBestUpdatesStore( backend: SqStoreRef, name: string, ): KvResult[LegacyBestLightClientUpdateStore] = + if backend.readOnly and not ? backend.hasTable(name): + return ok LegacyBestLightClientUpdateStore() + ? backend.exec(""" CREATE TABLE IF NOT EXISTS `""" & name & """` ( `period` INTEGER PRIMARY KEY, -- `SyncCommitteePeriod` @@ -181,7 +196,13 @@ proc initLegacyBestUpdatesStore( ); """) + const legacyKind = Base10.toString(ord(LightClientDataFork.Altair).uint) let + getStmt = backend.prepareStmt(""" + SELECT """ & legacyKind & """ AS `kind`, `update` + FROM `""" & name & """` + WHERE `period` = ?; + """, int64, (int64, seq[byte]), managed = false).expect("SQL query OK") putStmt = backend.prepareStmt(""" REPLACE INTO `""" & name & """` ( `period`, `update` @@ -201,21 +222,26 @@ proc initLegacyBestUpdatesStore( """, int64, void, managed = false).expect("SQL query OK") ok LegacyBestLightClientUpdateStore( + getStmt: getStmt, putStmt: putStmt, delStmt: delStmt, delFromStmt: delFromStmt, keepFromStmt: keepFromStmt) -func close(store: LegacyBestLightClientUpdateStore) = - store.putStmt.dispose() - store.delStmt.dispose() - store.delFromStmt.dispose() - store.keepFromStmt.dispose() +func close(store: var LegacyBestLightClientUpdateStore) = + store.getStmt.disposeSafe() + store.putStmt.disposeSafe() + store.delStmt.disposeSafe() + store.delFromStmt.disposeSafe() + store.keepFromStmt.disposeSafe() proc initBestUpdatesStore( backend: SqStoreRef, name, legacyAltairName: string, ): KvResult[BestLightClientUpdateStore] = + if backend.readOnly and not ? backend.hasTable(name): + return ok BestLightClientUpdateStore() + ? backend.exec(""" CREATE TABLE IF NOT EXISTS `""" & name & """` ( `period` INTEGER PRIMARY KEY, -- `SyncCommitteePeriod` @@ -223,7 +249,7 @@ proc initBestUpdatesStore( `update` BLOB -- `LightClientUpdate` (SSZ) ); """) - block: + if ? backend.hasTable(legacyAltairName): # SyncCommitteePeriod -> altair.LightClientUpdate const legacyKind = Base10.toString(ord(LightClientDataFork.Altair).uint) ? backend.exec(""" @@ -267,19 +293,20 @@ proc initBestUpdatesStore( delFromStmt: delFromStmt, keepFromStmt: keepFromStmt) -func close(store: BestLightClientUpdateStore) = - store.getStmt.dispose() - store.putStmt.dispose() - store.delStmt.dispose() - store.delFromStmt.dispose() - store.keepFromStmt.dispose() +func close(store: var BestLightClientUpdateStore) = + store.getStmt.disposeSafe() + store.putStmt.disposeSafe() + store.delStmt.disposeSafe() + store.delFromStmt.disposeSafe() + store.keepFromStmt.disposeSafe() proc getBestUpdate*( db: LightClientDataDB, period: SyncCommitteePeriod ): ForkedLightClientUpdate = doAssert period.isSupportedBySQLite + var update: (int64, seq[byte]) - for res in db.bestUpdates.getStmt.exec(period.int64, update): + template body: untyped = res.expect("SQL query OK") try: withAll(LightClientDataFork): @@ -297,9 +324,19 @@ proc getBestUpdate*( period, kind = update[0], exc = exc.msg return default(ForkedLightClientUpdate) + if distinctBase(db.bestUpdates.getStmt) != nil: + for res in db.bestUpdates.getStmt.exec(period.int64, update): + body + elif distinctBase(db.legacyBestUpdates.getStmt) != nil: + for res in db.legacyBestUpdates.getStmt.exec(period.int64, update): + body + else: + return default(ForkedLightClientUpdate) + func putBestUpdate*( db: LightClientDataDB, period: SyncCommitteePeriod, update: ForkedLightClientUpdate) = + doAssert not db.backend.readOnly # All `stmt` are non-nil doAssert period.isSupportedBySQLite withForkyUpdate(update): when lcDataFork > LightClientDataFork.None: @@ -341,6 +378,9 @@ proc putUpdateIfBetter*( proc initSealedPeriodsStore( backend: SqStoreRef, name: string): KvResult[SealedSyncCommitteePeriodStore] = + if backend.readOnly and not ? backend.hasTable(name): + return ok SealedSyncCommitteePeriodStore() + ? backend.exec(""" CREATE TABLE IF NOT EXISTS `""" & name & """` ( `period` INTEGER PRIMARY KEY -- `SyncCommitteePeriod` @@ -373,15 +413,17 @@ proc initSealedPeriodsStore( delFromStmt: delFromStmt, keepFromStmt: keepFromStmt) -func close(store: SealedSyncCommitteePeriodStore) = - store.containsStmt.dispose() - store.putStmt.dispose() - store.delFromStmt.dispose() - store.keepFromStmt.dispose() +func close(store: var SealedSyncCommitteePeriodStore) = + store.containsStmt.disposeSafe() + store.putStmt.disposeSafe() + store.delFromStmt.disposeSafe() + store.keepFromStmt.disposeSafe() func isPeriodSealed*( db: LightClientDataDB, period: SyncCommitteePeriod): bool = doAssert period.isSupportedBySQLite + if distinctBase(db.sealedPeriods.containsStmt) == nil: + return false var exists: int64 for res in db.sealedPeriods.containsStmt.exec(period.int64, exists): res.expect("SQL query OK") @@ -391,12 +433,14 @@ func isPeriodSealed*( func sealPeriod*( db: LightClientDataDB, period: SyncCommitteePeriod) = + doAssert not db.backend.readOnly # All `stmt` are non-nil doAssert period.isSupportedBySQLite let res = db.sealedPeriods.putStmt.exec(period.int64) res.expect("SQL query OK") func delNonFinalizedPeriodsFrom*( db: LightClientDataDB, minPeriod: SyncCommitteePeriod) = + doAssert not db.backend.readOnly # All `stmt` are non-nil doAssert minPeriod.isSupportedBySQLite block: let res = db.sealedPeriods.delFromStmt.exec(minPeriod.int64) @@ -411,6 +455,7 @@ func delNonFinalizedPeriodsFrom*( func keepPeriodsFrom*( db: LightClientDataDB, minPeriod: SyncCommitteePeriod) = + doAssert not db.backend.readOnly # All `stmt` are non-nil doAssert minPeriod.isSupportedBySQLite block: let res = db.sealedPeriods.keepFromStmt.exec(minPeriod.int64) diff --git a/beacon_chain/light_client_db.nim b/beacon_chain/light_client_db.nim index fe8551832..90fd2ede8 100644 --- a/beacon_chain/light_client_db.nim +++ b/beacon_chain/light_client_db.nim @@ -32,6 +32,7 @@ type Finalized = 1 # Latest finalized header LegacyLightClientHeadersStore = object + getStmt: SqliteStmt[int64, (int64, seq[byte])] putStmt: SqliteStmt[(int64, seq[byte]), void] LightClientHeadersStore = object @@ -59,9 +60,17 @@ type ## SyncCommitteePeriod -> altair.SyncCommittee ## Stores finalized `SyncCommittee` by sync committee period. +template disposeSafe(s: untyped): untyped = + if distinctBase(s) != nil: + s.dispose() + s = nil + proc initLegacyLightClientHeadersStore( backend: SqStoreRef, name: string): KvResult[LegacyLightClientHeadersStore] = + if backend.readOnly and not ? backend.hasTable(name): + return ok LegacyLightClientHeadersStore() + ? backend.exec(""" CREATE TABLE IF NOT EXISTS `""" & name & """` ( `kind` INTEGER PRIMARY KEY, -- `LightClientHeaderKey` @@ -69,7 +78,13 @@ proc initLegacyLightClientHeadersStore( ); """) + const legacyKind = Base10.toString(ord(LightClientDataFork.Altair).uint) let + getStmt = backend.prepareStmt(""" + SELECT """ & legacyKind & """ AS `kind`, `header` + FROM `""" & name & """` + WHERE `""" & name & """`.`kind` = ?; + """, int64, (int64, seq[byte]), managed = false).expect("SQL query OK") putStmt = backend.prepareStmt(""" REPLACE INTO `""" & name & """` ( `kind`, `header` @@ -78,14 +93,19 @@ proc initLegacyLightClientHeadersStore( .expect("SQL query OK") ok LegacyLightClientHeadersStore( + getStmt: getStmt, putStmt: putStmt) -func close(store: LegacyLightClientHeadersStore) = - store.putStmt.dispose() +func close(store: var LegacyLightClientHeadersStore) = + store.getStmt.disposeSafe() + store.putStmt.disposeSafe() proc initLightClientHeadersStore( backend: SqStoreRef, name, legacyAltairName: string): KvResult[LightClientHeadersStore] = + if backend.readOnly and not ? backend.hasTable(name): + return ok LightClientHeadersStore() + ? backend.exec(""" CREATE TABLE IF NOT EXISTS `""" & name & """` ( `key` INTEGER PRIMARY KEY, -- `LightClientHeaderKey` @@ -93,7 +113,7 @@ proc initLightClientHeadersStore( `header` BLOB -- `LightClientHeader` (SSZ) ); """) - block: + if ? backend.hasTable(legacyAltairName): # LightClientHeaderKey -> altair.LightClientHeader const legacyKind = Base10.toString(ord(LightClientDataFork.Altair).uint) ? backend.exec(""" @@ -121,15 +141,16 @@ proc initLightClientHeadersStore( getStmt: getStmt, putStmt: putStmt) -func close(store: LightClientHeadersStore) = - store.getStmt.dispose() - store.putStmt.dispose() +func close(store: var LightClientHeadersStore) = + store.getStmt.disposeSafe() + store.putStmt.disposeSafe() proc getLatestFinalizedHeader*( db: LightClientDB): ForkedLightClientHeader = const key = LightClientHeaderKey.Finalized + var header: (int64, seq[byte]) - for res in db.headers.getStmt.exec(key.int64, header): + template body: untyped = res.expect("SQL query OK") try: withAll(LightClientDataFork): @@ -147,8 +168,18 @@ proc getLatestFinalizedHeader*( key, kind = header[0], exc = exc.msg return default(ForkedLightClientHeader) + if distinctBase(db.headers.getStmt) != nil: + for res in db.headers.getStmt.exec(key.int64, header): + body + elif distinctBase(db.legacyHeaders.getStmt) != nil: + for res in db.legacyHeaders.getStmt.exec(key.int64, header): + body + else: + return default(ForkedLightClientHeader) + func putLatestFinalizedHeader*( db: LightClientDB, header: ForkedLightClientHeader) = + doAssert not db.backend.readOnly # All `stmt` are non-nil withForkyHeader(header): when lcDataFork > LightClientDataFork.None: block: @@ -161,6 +192,9 @@ func putLatestFinalizedHeader*( let res = db.legacyHeaders.putStmt.exec( (key.int64, SSZ.encode(forkyHeader))) res.expect("SQL query OK") + else: + # Keep legacy table at best Altair header. + discard block: let period = forkyHeader.beacon.slot.sync_committee_period doAssert period.isSupportedBySQLite @@ -171,6 +205,9 @@ func putLatestFinalizedHeader*( func initSyncCommitteesStore( backend: SqStoreRef, name: string): KvResult[SyncCommitteeStore] = + if backend.readOnly and not ? backend.hasTable(name): + return ok SyncCommitteeStore() + ? backend.exec(""" CREATE TABLE IF NOT EXISTS `""" & name & """` ( `period` INTEGER PRIMARY KEY, -- `SyncCommitteePeriod` @@ -199,14 +236,16 @@ func initSyncCommitteesStore( putStmt: putStmt, keepFromStmt: keepFromStmt) -func close(store: SyncCommitteeStore) = - store.getStmt.dispose() - store.putStmt.dispose() - store.keepFromStmt.dispose() +func close(store: var SyncCommitteeStore) = + store.getStmt.disposeSafe() + store.putStmt.disposeSafe() + store.keepFromStmt.disposeSafe() proc getSyncCommittee*( db: LightClientDB, period: SyncCommitteePeriod): Opt[altair.SyncCommittee] = doAssert period.isSupportedBySQLite + if distinctBase(db.syncCommittees.getStmt) == nil: + return Opt.none(altair.SyncCommittee) var syncCommittee: seq[byte] for res in db.syncCommittees.getStmt.exec(period.int64, syncCommittee): res.expect("SQL query OK") @@ -220,6 +259,7 @@ proc getSyncCommittee*( func putSyncCommittee*( db: LightClientDB, period: SyncCommitteePeriod, syncCommittee: altair.SyncCommittee) = + doAssert not db.backend.readOnly # All `stmt` are non-nil doAssert period.isSupportedBySQLite let res = db.syncCommittees.putStmt.exec( (period.int64, SSZ.encode(syncCommittee)))