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
Synchronization Primitives
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:
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