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.
This commit is contained in:
Etan Kissling 2023-01-18 03:06:23 +01:00 committed by GitHub
parent 073c544f0c
commit 9cceb1b4a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 121 additions and 36 deletions

View File

@ -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)

View File

@ -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)))