@@ -38,6 +38,7 @@ import Data.ByteString (ByteString)
3838import qualified Data.ByteString as BS
3939import qualified Data.ByteString.Builder as BB
4040import qualified Data.ByteString.Lazy as BSL
41+ import Data.IORef (IORef , atomicModifyIORef' , newIORef )
4142import Data.Int (Int64 )
4243import qualified Data.Map.Strict as Map
4344import 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.
166172sqlGarbageCollect ::
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
432491withStmt 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.
445496execWithInt64x3 ::
@@ -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
10131064sql_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
10181132dbBindBlob :: HasCallStack => DB. Statement -> DB. ParamIndex -> ByteString -> IO ()
0 commit comments