Skip to main content

reifydb_store_multi/store/
worker.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::collections::HashMap;
5
6use reifydb_codec::key::encoded::EncodedKey;
7use reifydb_core::{
8	common::CommitVersion,
9	event::{
10		EventBus,
11		metric::{MultiCommittedEvent, MultiDrop},
12	},
13	interface::store::EntryKind,
14};
15use reifydb_runtime::{
16	actor::{
17		context::Context,
18		mailbox::ActorRef,
19		system::{ActorConfig, ActorSpawner},
20		timers::TimerHandle,
21		traits::{Actor, Directive},
22	},
23	context::clock::{Clock, Instant},
24};
25use reifydb_value::value::duration::Duration;
26use tracing::{Span, debug, error, instrument};
27
28use super::{drop::find_keys_to_drop, pending::PendingDrops};
29use crate::tier::{
30	TierStorage, commit::buffer::MultiCommitBufferTier, persistent::MultiPersistentTier, read::MultiReadBufferTier,
31};
32
33#[derive(Debug, Clone)]
34pub struct DropWorkerConfig {
35	pub batch_size: usize,
36
37	pub flush_interval: Duration,
38}
39
40impl Default for DropWorkerConfig {
41	fn default() -> Self {
42		Self {
43			batch_size: 100,
44			flush_interval: Duration::from_milliseconds(50).unwrap(),
45		}
46	}
47}
48
49use reifydb_core::actors::drop::{DropMessage, DropRequest};
50
51pub struct DropActor {
52	storage: MultiCommitBufferTier,
53	event_bus: EventBus,
54	config: DropWorkerConfig,
55	clock: Clock,
56	persistent: Option<MultiPersistentTier>,
57	read: Option<MultiReadBufferTier>,
58	pending_drops: PendingDrops,
59}
60
61pub struct DropActorState {
62	pending_requests: Vec<DropRequest>,
63
64	last_flush: Instant,
65
66	_timer_handle: Option<TimerHandle>,
67
68	flush_count: u64,
69}
70
71impl DropActor {
72	pub fn new(
73		config: DropWorkerConfig,
74		storage: MultiCommitBufferTier,
75		event_bus: EventBus,
76		clock: Clock,
77		persistent: Option<MultiPersistentTier>,
78		read: Option<MultiReadBufferTier>,
79		pending_drops: PendingDrops,
80	) -> Self {
81		Self {
82			storage,
83			event_bus,
84			config,
85			clock,
86			persistent,
87			read,
88			pending_drops,
89		}
90	}
91
92	#[allow(clippy::too_many_arguments)]
93	pub fn spawn(
94		spawner: &ActorSpawner,
95		config: DropWorkerConfig,
96		storage: MultiCommitBufferTier,
97		event_bus: EventBus,
98		clock: Clock,
99		persistent: Option<MultiPersistentTier>,
100		read: Option<MultiReadBufferTier>,
101		pending_drops: PendingDrops,
102	) -> ActorRef<DropMessage> {
103		let actor = Self::new(config, storage, event_bus, clock, persistent, read, pending_drops);
104		spawner.spawn_coordination("drop-worker", actor).actor_ref().clone()
105	}
106
107	fn maybe_flush(&self, state: &mut DropActorState) {
108		if state.pending_requests.len() >= self.config.batch_size {
109			self.flush(state);
110		}
111	}
112
113	fn flush(&self, state: &mut DropActorState) {
114		if state.pending_requests.is_empty() && self.pending_drops.is_empty() {
115			return;
116		}
117
118		if !state.pending_requests.is_empty() {
119			Self::process_batch(&self.storage, &mut state.pending_requests, &self.event_bus);
120		}
121		self.pending_drops.purge(self.persistent.as_ref(), self.read.as_ref());
122		state.last_flush = self.clock.instant();
123
124		state.flush_count += 1;
125		if state.flush_count.is_multiple_of(100) {
126			self.storage.maintenance();
127		}
128	}
129
130	#[instrument(name = "drop::process_batch", level = "debug", skip_all, fields(num_requests = requests.len(), total_dropped))]
131	fn process_batch(storage: &MultiCommitBufferTier, requests: &mut Vec<DropRequest>, event_bus: &EventBus) {
132		let mut batches: HashMap<EntryKind, Vec<(EncodedKey, CommitVersion)>> = HashMap::new();
133
134		let mut drops_with_stats = Vec::new();
135		let mut max_pending_version = CommitVersion(0);
136
137		for request in requests.drain(..) {
138			let version_for_event = request.pending_version.unwrap_or(request.commit_version);
139			if version_for_event > max_pending_version {
140				max_pending_version = version_for_event;
141			}
142
143			match find_keys_to_drop(storage, request.table, request.key.as_ref(), request.pending_version) {
144				Ok(entries_to_drop) => {
145					for entry in entries_to_drop {
146						drops_with_stats.push(MultiDrop {
147							key: request.key.clone(),
148							value_bytes: entry.value_bytes,
149						});
150
151						batches.entry(request.table)
152							.or_default()
153							.push((entry.key, entry.version));
154					}
155				}
156				Err(e) => {
157					error!("Drop actor failed to find keys to drop: {}", e);
158				}
159			}
160		}
161
162		if !batches.is_empty()
163			&& let Err(e) = storage.drop(batches)
164		{
165			error!("Drop actor failed to execute drops: {}", e);
166		}
167
168		let total_dropped = drops_with_stats.len();
169		Span::current().record("total_dropped", total_dropped);
170
171		event_bus.emit(MultiCommittedEvent::new(vec![], vec![], drops_with_stats, max_pending_version));
172	}
173}
174
175impl Actor for DropActor {
176	type State = DropActorState;
177	type Message = DropMessage;
178
179	fn init(&self, ctx: &Context<Self::Message>) -> Self::State {
180		debug!("Drop actor started");
181
182		let timer_handle = ctx.schedule_repeat(Duration::from_milliseconds(10).unwrap(), DropMessage::Tick);
183
184		DropActorState {
185			pending_requests: Vec::with_capacity(self.config.batch_size),
186			last_flush: self.clock.instant(),
187			_timer_handle: Some(timer_handle),
188			flush_count: 0,
189		}
190	}
191
192	fn handle(&self, state: &mut Self::State, msg: Self::Message, ctx: &Context<Self::Message>) -> Directive {
193		if ctx.is_cancelled() {
194			self.flush(state);
195			return Directive::Stop;
196		}
197
198		match msg {
199			DropMessage::Request(request) => {
200				state.pending_requests.push(request);
201				self.maybe_flush(state);
202			}
203			DropMessage::Batch(requests) => {
204				state.pending_requests.extend(requests);
205				self.maybe_flush(state);
206			}
207			DropMessage::PurgePending => {
208				if state.last_flush.elapsed() >= self.config.flush_interval.to_std() {
209					self.flush(state);
210				}
211			}
212			DropMessage::Tick => {
213				if (!state.pending_requests.is_empty() || !self.pending_drops.is_empty())
214					&& state.last_flush.elapsed() >= self.config.flush_interval.to_std()
215				{
216					self.flush(state);
217				}
218			}
219			DropMessage::Shutdown => {
220				debug!("Drop actor received shutdown signal");
221
222				self.flush(state);
223				return Directive::Stop;
224			}
225		}
226
227		Directive::Continue
228	}
229
230	fn post_stop(&self) {
231		debug!("Drop actor stopped");
232	}
233
234	fn config(&self) -> ActorConfig {
235		ActorConfig::new().mailbox_capacity(4096 * 16)
236	}
237}