Skip to main content

Overview

The Indexer 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 the Committer class:
  1. Initialization: Committer creates the Indexer in run() with a configurable worker count
  2. Request routing: Committer calls process_requests() after batching index requests across XIDs
  3. Recovery trigger: Committer invokes recover_indexes() when it receives an INDEX_RECOVERY_TRIGGER message
  4. Reconciliation: Committer calls process_index_reconciliation() when it receives a RECONCILE_INDEX message

Key Components

Index Status States

The lifecycle of an index operation is tracked through three states:
State Transitions: Notes:
  • create_indexBUILDING: Default state when an index build is initiated
  • drop_index on index in _work_setABORTING: Build in progress, mark for abort
  • drop_index on index NOT in _work_setDELETING: Fresh drop, no build in progress
  • _reconcile_index() checks the current status and decides:
    • DELETING → calls _drop()DELETED
    • ABORTING → calls _commit_build() (truncate) → DELETED
    • BUILDING → 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:
Worker Loop (task()):
  1. Wait on condition variable _cv for work in _queue
  2. Pop key from queue
  3. Fetch IndexParams from _work_set
  4. If status is BUILDING: call _build() and add result to pending reconciliation
  5. If status is DELETING/ABORTING: add directly to pending reconciliation (with null root)
Workers use std::jthread with std::stop_token for graceful shutdown coordination.

Index Build Process (_build)

Phase 1: Setup
  1. Invalidate table cache at the creation XID
  2. Extract index column positions from IndexInfo (sorted by idx_position)
  3. Get mutable table reference
  4. Create empty B-tree root for the index
Phase 2: Look-Aside Index (if needed)
  • 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_tracker prevents race conditions with concurrent index creation
Phase 3: Table Scan

Index Reconciliation (_reconcile_index)

After the initial build, changes made to the table during the build must be applied: Extent Chain Processing:
Decision Logic:

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:
Counter Decrement Points:
  • build() - when index already READY (skip)
  • drop() - when marking existing work item as ABORTING
  • abort_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:
How it helps:
  • Secondary indexes store internal_row_id instead 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
Lifecycle:
  • 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
Coordination for Concurrent Creation:

Thread Safety

Lock Ordering (to prevent deadlocks):
  1. _m (work_set, queue)
  2. _table_idx_map_mtx
  3. _pending_reconciliation_map_mtx
  4. _xid_ddl_counter_map_mtx
  5. _look_aside_mutex
Patterns Used:
  • std::scoped_lock for acquiring multiple locks atomically
  • std::atomic<int> for DDL counters to minimize contention
  • Separate mutexes for independent data structures to allow parallelism

Error Handling and Edge Cases

Public API Summary