Skip to content

Commit e3544e6

Browse files
committed
Garbage collect orphaned EBs
1 parent 302c349 commit e3544e6

3 files changed

Lines changed: 147 additions & 20 deletions

File tree

ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/SQLite.hs

Lines changed: 134 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import Data.ByteString (ByteString)
3838
import qualified Data.ByteString as BS
3939
import qualified Data.ByteString.Builder as BB
4040
import qualified Data.ByteString.Lazy as BSL
41+
import Data.IORef (IORef, atomicModifyIORef', newIORef)
4142
import Data.Int (Int64)
4243
import qualified Data.Map.Strict as Map
4344
import qualified Data.Set as Set
@@ -102,16 +103,14 @@ newLeiosDBSQLite tracer dbPath = do
102103
notificationChan <- atomically newBroadcastTChan
103104
-- start a thread to sample the sizes of the volatile LeiosDB partition
104105
startVolatileStatsSampler tracer dbPath
105-
-- One root call context per maintenance operation, held for the handle's
106-
-- lifetime, so span ids stay unique across the calls of one node run. The
107-
-- names label the ChainDB background threads that drive each operation.
108106
gcRootCtx <- rootCallCtx "leiosdb-gc"
109107
copyRootCtx <- rootCallCtx "leiosdb-copy"
108+
noGCYetDoneRef <- newIORef True -- True until the first GC of this handle
110109
pure $
111110
LeiosDbHandle
112111
{ subscribeEbNotifications =
113112
atomically (dupTChan notificationChan)
114-
, leiosDbGarbageCollect = sqlGarbageCollect tracer gcRootCtx dbPath
113+
, leiosDbGarbageCollect = sqlGarbageCollect tracer gcRootCtx dbPath noGCYetDoneRef
115114
, leiosDbMarkAsImmutable = sqlMarkAsImmutable tracer copyRootCtx dbPath
116115
, open = openSQLiteConnection tracer dbPath notificationChan
117116
}
@@ -162,16 +161,76 @@ sqlMarkAsImmutable tracer rootCtx dbPath point = do
162161
dbBindBlob stmt 1 ebHash
163162
fromIntegral <$> readSingleInt64 stmt
164163

165-
-- | Stub implementation of 'leiosDbGarbageCollect': flush the WAL.
164+
execWithBlob :: HasCallStack => DB.Database -> String -> ByteString -> IO ()
165+
execWithBlob db sql blob =
166+
withStmt db sql $ \stmt -> do
167+
dbBindBlob stmt 1 blob
168+
dbStep1Safe stmt
169+
170+
-- | Implements 'leiosDbGarbageCollect': evict every volatile EB all of whose
171+
-- announcements are older than the given slot, then flush the WAL.
166172
sqlGarbageCollect ::
167173
HasCallStack =>
168-
Tracer IO TraceLeiosDb -> CallCtx IO -> FilePath -> SlotNo -> IO ()
169-
sqlGarbageCollect tracer rootCtx dbPath gcSlot =
174+
Tracer IO TraceLeiosDb -> CallCtx IO -> FilePath -> IORef Bool -> SlotNo -> IO ()
175+
sqlGarbageCollect tracer rootCtx dbPath noGCYetDoneRef gcSlot =
170176
gcSpan rootCtx "sqlGarbageCollect" (unSlotNo gcSlot) $ \gcCtx ->
171-
withMaintenanceConn dbPath $ \db ->
177+
withMaintenanceConn dbPath $ \db -> do
178+
-- check if we're doing the first ever GC during this node's run
179+
-- and flip the flag if so
180+
-- TODO(geo2a): move this configuration out of the IORef.
181+
firstGc <- atomicModifyIORef' noGCYetDoneRef (\b -> (False, b))
182+
hasWork <- gcSpan gcCtx "noopGuard" () $ \_ ->
183+
withStmt db sql_gc_has_work $ \stmt -> do
184+
dbBindInt64 stmt 1 slot
185+
(/= 0) <$> readSingleInt64 stmt
186+
when (hasWork || firstGc) $ do
187+
(evictedEbs, evictedEbTxs, evictedTxs) <-
188+
gcSpan gcCtx "evictionTransaction" () $ \txnCtx ->
189+
dbWithTransaction db $ do
190+
nTxsFromFirstGC <-
191+
if firstGc
192+
-- if the the first GC (for example, after a node restart),
193+
-- run the expensive traversal
194+
then gcSpan txnCtx "orphanTxsFullScan" () $ \_ ->
195+
withStmt db sql_gc_orphan_txs_full_scan dbStep1Safe >> DB.changes db
196+
else pure 0
197+
-- find txHashBytes that are only referenced by volatile EBs that are older
198+
-- than the garbage collection slot and stage them for GC
199+
gcSpan txnCtx "stageOrphanCandidates" () $ \_ ->
200+
withStmt db sql_gc_stage_orphan_candidates $ \stmt -> do
201+
dbBindInt64 stmt 1 slot
202+
dbStep1Safe stmt
203+
-- garbage collect rows of EbTxs
204+
nEbTxs <- gcSpan txnCtx "evictEbTxs" () $ \_ ->
205+
execWithInt64 db sql_gc_ebTxs slot >> DB.changes db
206+
-- garbage collect EBs
207+
nEbs <- gcSpan txnCtx "evictEbs" () $ \_ ->
208+
execWithInt64 db sql_gc_ebs slot >> DB.changes db
209+
-- finally, garbage collect txs that were staged before
210+
--
211+
-- TODO(geo2a): can we GC txs based on the data we get from GCing EBs?
212+
-- why is sql_gc_stage_orphan_candidates + sql_gc_orphan_txs is
213+
-- faster than sql_gc_orphan_txs_full_scan?
214+
nTxs <- gcSpan txnCtx "orphanTxs" () $ \_ ->
215+
withStmt db sql_gc_orphan_txs dbStep1Safe >> DB.changes db
216+
gcSpan txnCtx "clearCandidates" () $ \_ ->
217+
withStmt db sql_gc_clear_candidates dbStep1Safe
218+
let nTxsTotal = nTxs + nTxsFromFirstGC
219+
when (nEbs > 0 || nEbTxs > 0 || nTxsTotal > 0) $
220+
execWithInt64x3
221+
db
222+
sql_update_volatile_stats
223+
( negate (fromIntegral nEbs)
224+
, negate (fromIntegral nEbTxs)
225+
, negate (fromIntegral nTxsTotal)
226+
)
227+
pure (nEbs, nEbTxs, nTxsTotal)
228+
traceWith tracer TraceLeiosDbEvicted{evictedEbs, evictedEbTxs, evictedTxs}
172229
gcSpan gcCtx "walCheckpoint" () $ \_ ->
173230
dbExec db "PRAGMA wal_checkpoint(TRUNCATE);"
174231
where
232+
slot = fromIntegral (unSlotNo gcSlot)
233+
175234
gcSpan ::
176235
(Aeson.ToJSON arg, Aeson.ToJSON res) =>
177236
CallCtx IO -> CallName -> arg -> (CallCtx IO -> IO res) -> IO res
@@ -432,14 +491,6 @@ withStmt :: HasCallStack => DB.Database -> String -> (DB.Statement -> IO a) -> I
432491
withStmt db sql =
433492
MonadThrow.bracket (dbPrepare db (fromString sql)) dbFinalize
434493

435-
-- | Run a maintenance statement that takes a single BLOB parameter and returns
436-
-- no rows.
437-
execWithBlob :: HasCallStack => DB.Database -> String -> ByteString -> IO ()
438-
execWithBlob db sql blob =
439-
withStmt db sql $ \stmt -> do
440-
dbBindBlob stmt 1 blob
441-
dbStep1Safe stmt
442-
443494
-- | Run a maintenance statement that takes three INTEGER parameters and
444495
-- returns no rows.
445496
execWithInt64x3 ::
@@ -797,10 +848,10 @@ sql_schema =
797848
, " (SELECT 1 FROM ebTxs e JOIN ebs b ON b.ebHashBytes = e.ebHashBytes AND b.immutable = 1"
798849
, " WHERE e.txHashBytes = t.txHashBytes))"
799850
, "WHERE NOT EXISTS (SELECT 1 FROM leiosDbStats WHERE id = 0);"
800-
-- , -- Migration: the staging table of the removed garbage-collection path.
801-
-- -- Idempotent, so existing devnet databases shed it on the next open.
802-
-- -- TODO(geo2a): can we remove that?
803-
-- "DROP TABLE IF EXISTS gcTxCandidates;"
851+
, -- Garbage collection candidates.
852+
"CREATE TABLE IF NOT EXISTS gcTxCandidates ("
853+
, " txHashBytes BLOB NOT NULL PRIMARY KEY"
854+
, ");"
804855
]
805856

806857
-- | The 'ebTxs' rows of one EB hash. @?1@ is the ebHash blob.
@@ -1013,6 +1064,69 @@ sql_read_volatile_stats :: String
10131064
sql_read_volatile_stats =
10141065
"SELECT volatileEbs, volatileEbTxs, volatileTxs FROM leiosDbStats WHERE id = 0\n"
10151066

1067+
-- ** Garbage collection of the volatile partition
1068+
1069+
-- | Whether a GC at slot @?1@ would evict anything.
1070+
sql_gc_has_work :: String
1071+
sql_gc_has_work =
1072+
"SELECT EXISTS (SELECT 1 FROM ebs WHERE immutable = 0 AND ebSlot < ?1)\n\
1073+
\"
1074+
1075+
-- | The evictable EB hashes: every announcement is volatile and older than
1076+
-- the GC slot @?1@.
1077+
sql_gc_stale_hashes :: String
1078+
sql_gc_stale_hashes =
1079+
"SELECT DISTINCT cand.ebHashBytes FROM ebs cand\n\
1080+
\ WHERE cand.immutable = 0 AND cand.ebSlot < ?1\n\
1081+
\ AND NOT EXISTS\n\
1082+
\ (SELECT 1 FROM ebs\n\
1083+
\ WHERE ebs.ebHashBytes = cand.ebHashBytes\n\
1084+
\ AND (ebs.immutable = 1 OR ebs.ebSlot >= ?1))\n"
1085+
1086+
-- | Stage the txs of the stale EB hashes as orphan candidates. @?1@ is the GC
1087+
-- slot.
1088+
sql_gc_stage_orphan_candidates :: String
1089+
sql_gc_stage_orphan_candidates =
1090+
"INSERT OR IGNORE INTO gcTxCandidates (txHashBytes)\n\
1091+
\ SELECT DISTINCT txHashBytes FROM ebTxs WHERE ebHashBytes IN\n\
1092+
\ ("
1093+
<> sql_gc_stale_hashes
1094+
<> ")\n"
1095+
1096+
-- | Evict the body rows of the stale EB hashes. @?1@ is the GC slot.
1097+
sql_gc_ebTxs :: String
1098+
sql_gc_ebTxs =
1099+
"DELETE FROM ebTxs WHERE ebHashBytes IN\n\
1100+
\ ("
1101+
<> sql_gc_stale_hashes
1102+
<> ")\n"
1103+
1104+
-- | Evict volatile announcements older than the GC slot @?1@.
1105+
sql_gc_ebs :: String
1106+
sql_gc_ebs = "DELETE FROM ebs WHERE immutable = 0 AND ebSlot < ?\n"
1107+
1108+
-- | Reap staged candidate txs that no EB references any more.
1109+
sql_gc_orphan_txs :: String
1110+
sql_gc_orphan_txs =
1111+
"DELETE FROM txs WHERE txHashBytes IN\n\
1112+
\ (SELECT txHashBytes FROM gcTxCandidates)\n\
1113+
\ AND NOT EXISTS\n\
1114+
\ (SELECT 1 FROM ebTxs WHERE ebTxs.txHashBytes = txs.txHashBytes)\n\
1115+
\"
1116+
1117+
-- | Full-scan variant of 'sql_gc_orphan_txs', run once per handle on the
1118+
-- first GC: reaps orphans the candidate scheme cannot see (e.g. from before
1119+
-- 'gcTxCandidates' existed).
1120+
sql_gc_orphan_txs_full_scan :: String
1121+
sql_gc_orphan_txs_full_scan =
1122+
"DELETE FROM txs WHERE NOT EXISTS\n\
1123+
\ (SELECT 1 FROM ebTxs WHERE ebTxs.txHashBytes = txs.txHashBytes)\n\
1124+
\"
1125+
1126+
-- | Drop all staged candidates.
1127+
sql_gc_clear_candidates :: String
1128+
sql_gc_clear_candidates = "DELETE FROM gcTxCandidates\n"
1129+
10161130
-- * Low-level terminating SQLite functions
10171131

10181132
dbBindBlob :: HasCallStack => DB.Statement -> DB.ParamIndex -> ByteString -> IO ()

ouroboros-consensus/src/ouroboros-consensus/LeiosDemoDb/Trace.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ data TraceLeiosDb
3333
, copiedEbTxs :: !Int
3434
, copiedTxs :: !Int
3535
}
36+
| -- | Rows evicted by 'LeiosDbHandle.leiosDbGarbageCollect'.
37+
TraceLeiosDbEvicted
38+
{ evictedEbs :: !Int
39+
, evictedEbTxs :: !Int
40+
, evictedTxs :: !Int
41+
}
3642
| -- | A trace event for LeiosUtils.CallTrace spans
3743
TraceLeiosDbCall !SomeJsonCallTrace
3844
deriving Show

ouroboros-consensus/src/ouroboros-consensus/LeiosDemoTypes.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,13 @@ traceLeiosKernelToObject = \case
10941094
, "copiedEbTxs" .= copiedEbTxs
10951095
, "copiedTxs" .= copiedTxs
10961096
]
1097+
TraceLeiosDb TraceLeiosDbEvicted{evictedEbs, evictedEbTxs, evictedTxs} ->
1098+
mconcat
1099+
[ "kind" .= Aeson.String "LeiosDbEvicted"
1100+
, "evictedEbs" .= evictedEbs
1101+
, "evictedEbTxs" .= evictedEbTxs
1102+
, "evictedTxs" .= evictedTxs
1103+
]
10971104
-- The object carries @"kind": "Call"@ (from 'callTraceToObject'), matching
10981105
-- the forge loop's call traces, so one dashboard query shape covers both.
10991106
TraceLeiosDb (TraceLeiosDbCall (SomeJsonCallTrace ct)) ->

0 commit comments

Comments
 (0)