// Copyright (c) 2023-2025 ParadeDB, Inc. // // This file is part of ParadeDB - Postgres for Search and Analytics // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use crate::index::reader::index::SearchIndexReader; use crate::postgres::ParallelScanState; use pgrx::{pg_guard, pg_sys}; use std::collections::HashSet; use std::ptr::addr_of_mut; use tantivy::index::SegmentId; #[derive(Debug)] #[repr(transparent)] pub struct Spinlock(pg_sys::slock_t); impl Spinlock { #[inline(always)] pub fn init(&mut self) { unsafe { // SAFETY: `unsafe` due to normal FFI pg_sys::SpinLockInit(addr_of_mut!(self.0)); } } #[inline(always)] pub fn acquire(&mut self) -> impl Drop { AcquiredSpinLock::new(self) } } #[repr(transparent)] struct AcquiredSpinLock(*mut pg_sys::slock_t); impl AcquiredSpinLock { fn new(lock: &mut Spinlock) -> Self { unsafe { let addr = addr_of_mut!(lock.0); pg_sys::SpinLockAcquire(addr); Self(addr) } } } impl Drop for AcquiredSpinLock { #[inline(always)] fn drop(&mut self) { unsafe { pg_sys::SpinLockRelease(self.0); } } } #[pg_guard] pub unsafe extern "C" fn aminitparallelscan(target: *mut ::core::ffi::c_void) { let state = target.cast::(); (*state).init_mutex(); } #[pg_guard] pub unsafe extern "C" fn amparallelrescan(_scan: pg_sys::IndexScanDesc) {} #[cfg(any(feature = "pg14", feature = "pg15", feature = "pg16"))] #[pg_guard] pub unsafe extern "C" fn amestimateparallelscan() -> pg_sys::Size { ParallelScanState::size_of(u16::MAX as usize, &[]) } #[cfg(feature = "pg17")] #[pg_guard] pub unsafe extern "C" fn amestimateparallelscan(_nkeys: i32, _norderbys: i32) -> pg_sys::Size { // NB: in this function, we have no idea how many segments we have. We don't even know which // index we're querying. So we choose a, hopefully, large enough value at 65536, or u16::MAX ParallelScanState::size_of(u16::MAX as usize, &[]) } unsafe fn bm25_shared_state(scan: &pg_sys::IndexScanDescData) -> Option<&mut ParallelScanState> { if scan.parallel_scan.is_null() { None } else { scan.parallel_scan .cast::() .add((*scan.parallel_scan).ps_offset) .cast::() .as_mut() } } pub unsafe fn maybe_init_parallel_scan( scan: pg_sys::IndexScanDesc, searcher: &SearchIndexReader, ) -> Option { if unsafe { (*scan).parallel_scan.is_null() } { // not a parallel scan, so there's nothing to initialize return None; } let state = get_bm25_scan_state(&scan)?; let worker_number = unsafe { pg_sys::ParallelWorkerNumber }; let _mutex = state.acquire_mutex(); if worker_number == -1 { // ParallelWorkerNumber -1 is the main backend, which is where we'll set up // our shared memory information. The mutex was already initialized, directly, in // `aminitparallelscan()` state.init_without_mutex(searcher.segment_readers(), &[]); } Some(worker_number) } pub unsafe fn maybe_claim_segment(scan: pg_sys::IndexScanDesc) -> Option { let state = get_bm25_scan_state(&scan)?; let _mutex = state.acquire_mutex(); if state.remaining_segments() == 0 { // no more to claim None } else { // claim the next one let remaining_segments = state.decrement_remaining_segments(); Some(state.segment_id(remaining_segments)) } } pub unsafe fn list_segment_ids(scan: pg_sys::IndexScanDesc) -> Option> { Some( get_bm25_scan_state(&scan)? .segments() .keys() .cloned() .collect(), ) } fn get_bm25_scan_state(scan: &pg_sys::IndexScanDesc) -> Option<&mut ParallelScanState> { unsafe { assert!(!scan.is_null()); let scan = scan.as_mut().unwrap_unchecked(); bm25_shared_state(scan) } }