Overview
TheIndexer class is the core component responsible for building and managing secondary indexes on tables in the Springtail database system. It operates as a multi-threaded service within the springtail::committer namespace, handling index creation, deletion, and abort operations asynchronously via worker threads. Recovery and reconciliation operations are synchronous—the Committer waits for them to complete before proceeding.
Responsibilities
- Building secondary indexes by scanning table data
- Building look-aside indexes to reduce write amplification (see Look-Aside Index for details)
- Handling index drops - both immediate drops and drops while a build is in progress
- Recovering incomplete index operations after system crashes or shutdowns
- Reconciling indexes with data changes that occurred during the build phase
- Coordinating with committer to signal when index operations are ready for commit
Integration with Committer
The Indexer is owned and managed by theCommitter class:
- Initialization: Committer creates the Indexer in
run()with a configurable worker count - Request routing: Committer calls
process_requests()after batching index requests across XIDs - Recovery trigger: Committer invokes
recover_indexes()when it receives anINDEX_RECOVERY_TRIGGERmessage - Reconciliation: Committer calls
process_index_reconciliation()when it receives aRECONCILE_INDEXmessage
Key Components
Index Status States
The lifecycle of an index operation is tracked through three states:create_index→BUILDING: Default state when an index build is initiateddrop_indexon index in_work_set→ABORTING: Build in progress, mark for abortdrop_indexon index NOT in_work_set→DELETING: Fresh drop, no build in progress_reconcile_index()checks the current status and decides:DELETING→ calls_drop()→DELETEDABORTING→ calls_commit_build()(truncate) →DELETEDBUILDING→ calls_commit_build()(finalize) →READY
Core Data Structures
IndexParams
Encapsulates all parameters needed for an index operation:IndexState
Captures the state of an index after initial build phase, used during reconciliation:Key Type
Unique identifier for work items:Internal Maps and Queues
| Member | Type | Purpose |
|---|---|---|
_work_set | unordered_map<Key, IndexParams> | Active index operations keyed by (db_id, index_id) |
_queue | queue<Key> | FIFO queue of keys for worker threads to process |
_pending_idx_reconciliation_map | db_id → xid → list<IndexState> | Indexes awaiting reconciliation, grouped by database and XID |
_table_idx_map | db_id → table_id → list<Key> | Maps tables to their in-progress index builds (for abort_indexes) |
_xid_ddl_counter_map | xid → atomic<int> | Tracks pending DDL operations per transaction |
_look_aside_build_tracker | table_id → bool | Prevents duplicate look-aside index builds when multiple indexes created concurrently |
Synchronization Primitives
| Mutex | Protects |
|---|---|
_m | _work_set, _queue, condition variable coordination |
_pending_reconciliation_map_mtx | _pending_idx_reconciliation_map |
_table_idx_map_mtx | _table_idx_map |
_xid_ddl_counter_map_mtx | _xid_ddl_counter_map |
_look_aside_mutex | _look_aside_build_tracker |
Data Flow
Index Creation Flow
Index Drop Flow
Index Recovery Flow
Abort Indexes Flow (Table Resync)
Implementation Details
Worker Thread Model
The Indexer spawns a configurable number of worker threads at construction:task()):
- Wait on condition variable
_cvfor work in_queue - Pop key from queue
- Fetch
IndexParamsfrom_work_set - If status is
BUILDING: call_build()and add result to pending reconciliation - If status is
DELETING/ABORTING: add directly to pending reconciliation (with null root)
std::jthread with std::stop_token for graceful shutdown coordination.
Index Build Process (_build)
Phase 1: Setup
- Invalidate table cache at the creation XID
- Extract index column positions from
IndexInfo(sorted byidx_position) - Get mutable table reference
- Create empty B-tree root for the index
- Look-aside maps
internal_row_id → (extent_id, row_id_within_extent) - Only the first secondary index on a table builds it
_look_aside_build_trackerprevents race conditions with concurrent index creation
Index Reconciliation (_reconcile_index)
After the initial build, changes made to the table during the build must be applied:
Extent Chain Processing:
| Work Item Status | Action |
|---|---|
DELETING | Call _drop() directly |
ABORTING | Call _commit_build() to truncate and mark deleted |
BUILDING | Process extent chain, then call _commit_build() to finalize |
Index Commit (_commit_build)
Finalizes an index build or abort:
DDL Counter Management
The counter ensures the Committer only receives reconciliation notifications when ALL index operations for a transaction are complete:build()- when index already READY (skip)drop()- when marking existing work item as ABORTINGabort_indexes()- after marking all table indexes as ABORTING_add_to_pending_reconciliation()- after build/worker processing completes
Index Drop (_drop)
Handles direct index deletion:
Look-Aside Index
Structure:- Secondary indexes store
internal_row_idinstead of direct(extent_id, row_offset)pointers - When a data extent is rewritten, only the look-aside index needs to be updated with the new physical location
- Secondary indexes remain unchanged, eliminating cascading updates
- Created: With the first secondary index on a table
- Updated: During index reconciliation when extents change
- Dropped: When the last secondary index on a table is dropped
Thread Safety
Lock Ordering (to prevent deadlocks):_m(work_set, queue)_table_idx_map_mtx_pending_reconciliation_map_mtx_xid_ddl_counter_map_mtx_look_aside_mutex
std::scoped_lockfor acquiring multiple locks atomicallystd::atomic<int>for DDL counters to minimize contention- Separate mutexes for independent data structures to allow parallelism
Error Handling and Edge Cases
| Scenario | Handling |
|---|---|
| Index already READY at build time | Skip processing, decrement DDL counter |
| Stop requested during build | Truncate partial B-tree, return null root for recovery |
| Drop while building | Mark ABORTING, worker detects via periodic check |
| Table dropped before index drop | Mark index DELETED without truncating |
| Queue unavailable during reconciliation | Abort reconciliation (will retry on recovery) |
| Table resync during build | Index marked DELETED via abort_indexes |
Public API Summary
| Method | Description |
|---|---|
process_requests(db_id, xid, requests) | Entry point for index create/drop/abort operations |
recover_indexes(db_id) | Recovers incomplete operations after restart |
process_index_reconciliation(db_id, reconcile_xid, end_xid) | Finalizes pending index operations |
abort_indexes(db_id, table_id, xid) | Aborts all index builds for a table (used during resync) |