trellis-pg 0.1.0.pre.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1281 @@
1
+ //! The Ruby binding's native extension: a thin Magnus wrapper over
2
+ //! [`trellis::BlockingTrellis`] (`docs/decisions/0010-embeddable-clients.md`).
3
+ //!
4
+ //! Only `lib/trellis/pg.rb` calls these; the public API, defaults, the
5
+ //! module-level singleton and the value classes live in the Ruby files under
6
+ //! `lib/`. The contract here is deliberately narrow:
7
+ //!
8
+ //! - **Every call that can block releases the GVL, and can be interrupted**
9
+ //! (ADR-0010 decision 3). The calling Ruby thread submits the call with
10
+ //! `BlockingTrellis`'s `start_*` methods, which return at once, and waits
11
+ //! for the reply itself with the GVL released: it polls the reply as a
12
+ //! future, and parks on a condition variable that the engine's reply (through
13
+ //! the future's waker) and Ruby's unblocking function both signal. No thread
14
+ //! is started for a call. An interrupt (`Thread#kill`, `Thread#raise`, a
15
+ //! signal) returns control to Ruby at once and drops the reply; the engine
16
+ //! ends the call by its own 30-second deadline, and the server stops what
17
+ //! the call started (issue #599). See [`wait_without_gvl`]. The one wait
18
+ //! with a thread behind it is [`connect`]'s: it runs `BlockingTrellis::connect`
19
+ //! (a bare blocking wait on the database) on a thread of its own, once per
20
+ //! connect.
21
+ //! - **A handle refuses to be used from any process but the one that
22
+ //! connected it.** Rust threads don't cross `fork`, so a handle a child
23
+ //! inherits has nothing left to answer it. Every call checks the pid first
24
+ //! and raises `Trellis::ForkedHandleError`, and a forked copy is never
25
+ //! dropped either (see [`Handle`]'s `Drop`).
26
+ //! - **A process forked while its parent had an engine running can't
27
+ //! connect one of its own** (issue #600): it may have inherited a lock one
28
+ //! of the parent's engine threads held, which nothing in it can release.
29
+ //! [`connect`] raises `Trellis::ForkedHandleError` instead of risking the
30
+ //! hang. See the [`engine`] module.
31
+ //! - **Only plain data crosses** (decision 4). The flattening is
32
+ //! `trellis-embed`'s; this crate turns its plain values into hashes, and
33
+ //! `lib/trellis/pg.rb` turns those into `Data` objects. Words become symbols
34
+ //! (statuses, quarantine states, `apply` outcome kinds, `self_check` outcomes and divergence kinds), but only
35
+ //! words from the closed sets `trellis-embed` lists, which [`init`] interns
36
+ //! up front, never a string read from the database.
37
+ //! - **Errors cross as `(code, message)`.** Ruby's `Trellis::Error.from_native`
38
+ //! picks the exception class for the code, so the code-to-class map lives in
39
+ //! one place, `lib/trellis/error.rb`.
40
+ //!
41
+ //! The crate is a member of the repository's Cargo workspace, but everything
42
+ //! in it sits behind the `ruby` feature; see `Cargo.toml` for why.
43
+
44
+ #![cfg(feature = "ruby")]
45
+
46
+ mod engine;
47
+
48
+ use std::ffi::c_void;
49
+ use std::future::Future;
50
+ use std::panic::{AssertUnwindSafe, catch_unwind};
51
+ use std::pin::Pin;
52
+ use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, RwLock};
53
+ use std::task::{Context, Poll, Wake, Waker};
54
+ use std::time::Duration;
55
+
56
+ use engine::{Engine, ForkedWhileRunning};
57
+ use magnus::prelude::*;
58
+ use magnus::{
59
+ Error, Exception, ExceptionClass, IntoValue, RArray, RClass, RHash, RModule, Ruby,
60
+ StaticSymbol, Value, function, method,
61
+ };
62
+ use trellis::{BlockingTrellis, Config, ErrorCode, PendingCall, TrellisOptions};
63
+ use trellis_embed::{
64
+ DIVERGENCE_KINDS, ERROR_CODES, PlainApplied, PlainBackfillFailure, PlainCaptureFailure,
65
+ PlainCaptureWait, PlainConfig, PlainCorrectingNode, PlainDefinitionStatus,
66
+ PlainDefinitionSummary, PlainDivergence, PlainDrainFailure, PlainError, PlainHeldKeys,
67
+ PlainPoisonEntry, PlainQuarantineEntry, PlainSamplePage, PlainSelfCheckJob,
68
+ PlainSelfCheckReport, PlainSourceIndexGap, SELF_CHECK_MODES, SELF_CHECK_OUTCOMES,
69
+ SELF_CHECK_STATES, capture_failure_kind_names, decode_cursor, decode_watermark,
70
+ encode_watermark, index_role_names, quarantine_state_names, require_transform_statement,
71
+ self_check_mode, system_time_from_epoch_micros, transform_status_names,
72
+ };
73
+
74
+ /// What a blocking call produces: its value, or the engine's error as plain
75
+ /// data, raised as a `Trellis::Error` once the GVL is back.
76
+ type Reply<T> = Result<T, PlainError>;
77
+
78
+ /// The live instance a [`Handle`] wraps. `shutdown` takes it out, so a call
79
+ /// after shutdown gets an error rather than a hang. The lock is read to submit
80
+ /// a call and written by `shutdown`, and is never held while waiting for a
81
+ /// reply. Ruby code never runs while it is held (the holder is native code
82
+ /// that releases no GVL), so a thread can't be switched out holding it.
83
+ type Shared = Arc<RwLock<Option<Engine>>>;
84
+
85
+ /// One connected Trellis instance, owned by Ruby as `Trellis::Native::Handle`
86
+ /// and held by the `Trellis` module's singleton.
87
+ ///
88
+ /// Calls from several Ruby threads don't wait on each other here: each waits
89
+ /// for its own reply on its own thread, and the [`BlockingTrellis`] runs them
90
+ /// in parallel.
91
+ #[magnus::wrap(class = "Trellis::Native::Handle", free_immediately, size)]
92
+ struct Handle {
93
+ trellis: Shared,
94
+ /// The OS process that connected (ADR-0010 decision 3's pid guard).
95
+ owner_pid: u32,
96
+ }
97
+
98
+ impl Drop for Handle {
99
+ /// A handle garbage-collected in a forked child is leaked, not dropped.
100
+ /// Dropping an [`Engine`] shuts it down, and in a child neither its
101
+ /// threads nor the runtime serving its job channel exist: the handle is a
102
+ /// copy of the parent's state, and the only safe thing to do with it is
103
+ /// leave it alone. In the owning process the drop is the ordinary
104
+ /// backstop: the engine shuts down on a thread of its own, without
105
+ /// blocking the GC that ran it.
106
+ fn drop(&mut self) {
107
+ if std::process::id() != self.owner_pid {
108
+ std::mem::forget(Arc::clone(&self.trellis));
109
+ }
110
+ }
111
+ }
112
+
113
+ impl Handle {
114
+ /// Submits the call `start` makes against the live instance, waits for
115
+ /// its reply with the GVL released, or raises why it can't.
116
+ fn call<R>(
117
+ &self,
118
+ ruby: &Ruby,
119
+ start: impl FnOnce(&BlockingTrellis) -> PendingCall<R>,
120
+ ) -> Result<R, Error> {
121
+ let pending = self.with_engine(ruby, start)?;
122
+ wait_without_gvl(pending)?.map_err(|err| raise(ruby, err))
123
+ }
124
+
125
+ /// Runs `read` against the live instance without waiting on anything, or
126
+ /// raises why it can't (including [`Handle::check_pid`]'s refusal, first).
127
+ fn with_engine<R>(
128
+ &self,
129
+ ruby: &Ruby,
130
+ read: impl FnOnce(&BlockingTrellis) -> R,
131
+ ) -> Result<R, Error> {
132
+ self.check_pid(ruby)?;
133
+ let guard = self.trellis.read().map_err(|_| raise(ruby, poisoned()))?;
134
+ let trellis = guard.as_ref().ok_or_else(|| {
135
+ raise(
136
+ ruby,
137
+ PlainError::new(
138
+ ErrorCode::Validation,
139
+ "this Trellis handle has been shut down",
140
+ ),
141
+ )
142
+ })?;
143
+ Ok(read(trellis))
144
+ }
145
+
146
+ /// Raises `Trellis::ForkedHandleError` unless this is the process that
147
+ /// connected. Checked before anything else a call does, so a forked child
148
+ /// never reaches a lock, a channel or a thread it inherited.
149
+ fn check_pid(&self, ruby: &Ruby) -> Result<(), Error> {
150
+ let pid = std::process::id();
151
+ if pid == self.owner_pid {
152
+ return Ok(());
153
+ }
154
+ Err(forked_handle_error(
155
+ ruby,
156
+ format!(
157
+ "this Trellis handle was connected by process {}, and this is process {pid}: \
158
+ a handle does not survive fork, so connect a new one in this process \
159
+ (after forking: Puma's before_worker_boot, Unicorn's after_fork, Passenger's \
160
+ starting_worker_process; \"Forking servers\" in clients/ruby/README.md \
161
+ covers preload_app! and fork_worker)",
162
+ self.owner_pid
163
+ ),
164
+ ))
165
+ }
166
+
167
+ fn owner_pid(&self) -> u32 {
168
+ self.owner_pid
169
+ }
170
+
171
+ fn migrate(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
172
+ rb_self.call(ruby, BlockingTrellis::start_migrate)
173
+ }
174
+
175
+ /// Registers the `TRANSFORM` statement `text`.
176
+ ///
177
+ /// `BlockingTrellis::apply` takes every statement form, so `text` is
178
+ /// checked first with `trellis::statement_kind` (through
179
+ /// `trellis-embed`): any other form is a `validation` error and is never
180
+ /// applied, and text that doesn't parse is the `parse` error `apply`
181
+ /// would return.
182
+ fn define(ruby: &Ruby, rb_self: &Self, text: String) -> Result<String, Error> {
183
+ rb_self.check_pid(ruby)?;
184
+ require_transform_statement(&text).map_err(|err| raise(ruby, err))?;
185
+ let target_table = rb_self
186
+ .call(ruby, |trellis| trellis.start_apply(&text))?
187
+ .into_transform();
188
+ // Unreachable: the check above is `apply`'s own parser. Kept as
189
+ // defence, so a mistake there is an error rather than a panic.
190
+ let target_table = target_table.ok_or_else(|| {
191
+ raise(
192
+ ruby,
193
+ PlainError::new(
194
+ ErrorCode::Internal,
195
+ "define applied a statement that registered no transform",
196
+ ),
197
+ )
198
+ })?;
199
+ Ok(target_table)
200
+ }
201
+
202
+ /// `target_table`'s status, or `nil` when no definition writes it.
203
+ fn status(ruby: &Ruby, rb_self: &Self, target_table: String) -> Result<Option<RHash>, Error> {
204
+ let status = rb_self
205
+ .call(ruby, |trellis| trellis.start_status(&target_table))?
206
+ .map(|status| PlainDefinitionStatus::from(&status));
207
+ status.map(|status| status_hash(ruby, status)).transpose()
208
+ }
209
+
210
+ /// Runs one statement of Trellis's grammar, whatever its form, and
211
+ /// reports what it did as `{kind:, target_table:, columns:, added:,
212
+ /// dropped:, altered:}`: `kind` is one of [`PlainApplied::KINDS`],
213
+ /// and each other field is set only for the kinds that carry it (see
214
+ /// [`applied_hash`]).
215
+ fn apply(ruby: &Ruby, rb_self: &Self, text: String) -> Result<RHash, Error> {
216
+ let applied = rb_self.call(ruby, |trellis| trellis.start_apply(&text))?;
217
+ applied_hash(ruby, PlainApplied::from(&applied))
218
+ }
219
+
220
+ /// Every registered transform definition, oldest first.
221
+ fn definitions(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
222
+ let summaries = rb_self
223
+ .call(ruby, BlockingTrellis::start_definitions)?
224
+ .iter()
225
+ .map(PlainDefinitionSummary::from)
226
+ .collect::<Vec<_>>();
227
+ array(ruby, summaries, definition_summary_hash)
228
+ }
229
+
230
+ /// The SQL to append to a schema-only dump of the database, so a load
231
+ /// of the dump carries this instance's definitions
232
+ /// ([`BlockingTrellis::dump_definitions_sql`]).
233
+ fn dump_definitions_sql(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
234
+ rb_self.call(ruby, BlockingTrellis::start_dump_definitions_sql)
235
+ }
236
+
237
+ /// Rebuilds every applying definition that reads `source_table`
238
+ /// (`live -> backfilling`).
239
+ fn request_backfill(ruby: &Ruby, rb_self: &Self, source_table: String) -> Result<(), Error> {
240
+ rb_self.call(ruby, |trellis| {
241
+ trellis.start_request_backfill(&source_table)
242
+ })
243
+ }
244
+
245
+ /// Releases one key `transform` holds in quarantine: `source_table`
246
+ /// (either spelling) and `key` as `sample_quarantined` reports them.
247
+ fn release_key(
248
+ ruby: &Ruby,
249
+ rb_self: &Self,
250
+ transform: String,
251
+ source_table: String,
252
+ key: String,
253
+ ) -> Result<(), Error> {
254
+ rb_self.call(ruby, |trellis| {
255
+ trellis.start_release_key(&transform, &source_table, &key)
256
+ })
257
+ }
258
+
259
+ /// Every key poisoned after `watermark_micros` (epoch microseconds),
260
+ /// oldest first.
261
+ fn poisoned_since(ruby: &Ruby, rb_self: &Self, watermark_micros: i64) -> Result<RArray, Error> {
262
+ rb_self.check_pid(ruby)?;
263
+ let watermark =
264
+ system_time_from_epoch_micros(watermark_micros).map_err(|err| raise(ruby, err))?;
265
+ let entries = rb_self
266
+ .call(ruby, |trellis| trellis.start_poisoned_since(watermark))?
267
+ .iter()
268
+ .map(PlainPoisonEntry::from)
269
+ .collect::<Vec<_>>();
270
+ array(ruby, entries, poison_entry_hash)
271
+ }
272
+
273
+ /// Every quarantined transform and paused column.
274
+ fn quarantined(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
275
+ let entries = rb_self
276
+ .call(ruby, BlockingTrellis::start_quarantined)?
277
+ .iter()
278
+ .map(PlainQuarantineEntry::from)
279
+ .collect::<Vec<_>>();
280
+ array(ruby, entries, quarantine_entry_hash)
281
+ }
282
+
283
+ /// One target's state, by its `transform` or `transform.column` address.
284
+ fn quarantine_status(ruby: &Ruby, rb_self: &Self, target: String) -> Result<RHash, Error> {
285
+ let entry = rb_self.call(ruby, |trellis| trellis.start_quarantine_status(&target))?;
286
+ quarantine_entry_hash(ruby, PlainQuarantineEntry::from(&entry))
287
+ }
288
+
289
+ /// Up to `limit` of `target`'s quarantined rows, after the opaque
290
+ /// `cursor` (`nil` for the first page).
291
+ fn sample_quarantined(
292
+ ruby: &Ruby,
293
+ rb_self: &Self,
294
+ target: String,
295
+ cursor: Option<String>,
296
+ limit: i64,
297
+ ) -> Result<RHash, Error> {
298
+ rb_self.check_pid(ruby)?;
299
+ let after = decode_cursor(cursor.as_deref()).map_err(|err| raise(ruby, err))?;
300
+ let samples = rb_self.call(ruby, |trellis| {
301
+ trellis.start_sample_quarantined(&target, after, limit)
302
+ })?;
303
+ let page = PlainSamplePage::new(&samples, cursor.as_deref());
304
+ let samples = array(ruby, page.samples, |ruby, sample| {
305
+ record(
306
+ ruby,
307
+ [
308
+ ("src_table", ruby.into_value(sample.src_table)),
309
+ ("key", ruby.into_value(sample.key)),
310
+ ("error_message", ruby.into_value(sample.error_message)),
311
+ ],
312
+ )
313
+ })?;
314
+ record(
315
+ ruby,
316
+ [
317
+ ("samples", samples.as_value()),
318
+ ("next_cursor", ruby.into_value(page.next_cursor)),
319
+ ],
320
+ )
321
+ }
322
+
323
+ /// Whether any drain worker in the fleet is alive.
324
+ fn has_live_drain_workers(ruby: &Ruby, rb_self: &Self) -> Result<bool, Error> {
325
+ rb_self.call(ruby, BlockingTrellis::start_has_live_drain_workers)
326
+ }
327
+
328
+ /// Whether this instance's staging worker is alive anywhere in the fleet.
329
+ fn has_live_staging_worker(ruby: &Ruby, rb_self: &Self) -> Result<bool, Error> {
330
+ rb_self.call(ruby, BlockingTrellis::start_has_live_staging_worker)
331
+ }
332
+
333
+ /// Pauses now the readers of every table whose capture was changed
334
+ /// outside Trellis (#1181), and returns the tables.
335
+ fn detect_capture_disruptions(ruby: &Ruby, rb_self: &Self) -> Result<Vec<String>, Error> {
336
+ rb_self.call(ruby, BlockingTrellis::start_detect_capture_disruptions)
337
+ }
338
+
339
+ /// A read-your-writes token, as an opaque string.
340
+ fn watermark_token(ruby: &Ruby, rb_self: &Self) -> Result<String, Error> {
341
+ rb_self
342
+ .call(ruby, BlockingTrellis::start_watermark_token)
343
+ .map(encode_watermark)
344
+ }
345
+
346
+ /// Waits up to `timeout_ms` for every change committed at or before
347
+ /// `token` to reach its targets, and at most the call's 30-second
348
+ /// deadline. Other calls on the handle don't queue behind it.
349
+ fn await_converged(
350
+ ruby: &Ruby,
351
+ rb_self: &Self,
352
+ token: String,
353
+ timeout_ms: u64,
354
+ ) -> Result<(), Error> {
355
+ rb_self.check_pid(ruby)?;
356
+ let token = decode_watermark(&token).map_err(|err| raise(ruby, err))?;
357
+ rb_self.call(ruby, |trellis| {
358
+ trellis.start_await_converged(token, Duration::from_millis(timeout_ms))
359
+ })
360
+ }
361
+
362
+ /// Starts a background check of `target_table` against a fresh recompute
363
+ /// from the source, in `mode` (`"standard"` or `"strict"`), waiting up to
364
+ /// `timeout_ms` per convergence await its pages make, and returns the job
365
+ /// at once. A target whose job is still running gets that job back.
366
+ fn self_check(
367
+ ruby: &Ruby,
368
+ rb_self: &Self,
369
+ target_table: String,
370
+ mode: String,
371
+ timeout_ms: u64,
372
+ ) -> Result<RHash, Error> {
373
+ rb_self.check_pid(ruby)?;
374
+ let mode = self_check_mode(&mode).map_err(|err| raise(ruby, err))?;
375
+ let job = rb_self.call(ruby, |trellis| {
376
+ trellis.start_self_check(&target_table, mode, Duration::from_millis(timeout_ms))
377
+ })?;
378
+ self_check_job_hash(ruby, PlainSelfCheckJob::from(&job))
379
+ }
380
+
381
+ /// The `self_check` job `id`, or `nil` when there is none: a newer
382
+ /// `self_check` of its target replaced it, or its transform was dropped.
383
+ fn self_check_job(ruby: &Ruby, rb_self: &Self, id: i64) -> Result<Option<RHash>, Error> {
384
+ rb_self.check_pid(ruby)?;
385
+ let job = rb_self.call(ruby, |trellis| trellis.start_self_check_job(id))?;
386
+ job.map(|job| self_check_job_hash(ruby, PlainSelfCheckJob::from(&job)))
387
+ .transpose()
388
+ }
389
+
390
+ /// The configuration the handle connected with, all but the connection
391
+ /// string (see [`PlainConfig`]).
392
+ fn config(ruby: &Ruby, rb_self: &Self) -> Result<RHash, Error> {
393
+ let config = rb_self.with_engine(ruby, |trellis| PlainConfig::from(trellis.config()))?;
394
+ record(
395
+ ruby,
396
+ [
397
+ ("schema", ruby.into_value(config.schema)),
398
+ ("target_schema", ruby.into_value(config.target_schema)),
399
+ ("pool_max_size", ruby.into_value(config.pool_max_size)),
400
+ (
401
+ "pool_wait_timeout_ms",
402
+ ruby.into_value(config.pool_wait_timeout_ms),
403
+ ),
404
+ ],
405
+ )
406
+ }
407
+
408
+ /// Stops the instance's background work and joins its runtime thread.
409
+ /// Idempotent: shutting down a handle that is already shut down is fine.
410
+ ///
411
+ /// Calls in flight on other threads are cancelled, not waited for (see
412
+ /// `BlockingTrellis::shutdown`), so a call stuck on a lock doesn't hold
413
+ /// this up. Interrupting the wait doesn't stop the shutdown: it finishes
414
+ /// on a thread of its own (see [`engine::Stopping`]).
415
+ fn shutdown(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
416
+ rb_self.check_pid(ruby)?;
417
+ let engine = rb_self
418
+ .trellis
419
+ .write()
420
+ .map_err(|_| raise(ruby, poisoned()))?
421
+ .take();
422
+ match engine {
423
+ Some(engine) => {
424
+ wait_without_gvl(engine.start_shutdown())?.map_err(|err| raise(ruby, err))
425
+ }
426
+ None => Ok(()),
427
+ }
428
+ }
429
+ }
430
+
431
+ /// Connects a new instance. Every option is required here: the defaults are
432
+ /// `lib/trellis/pg.rb`'s to document, and nothing falls back to the environment.
433
+ ///
434
+ /// Raises `Trellis::ForkedHandleError`, before starting anything, in a
435
+ /// process forked while its parent had an engine running (issue #600).
436
+ fn connect(
437
+ ruby: &Ruby,
438
+ url: String,
439
+ schema: String,
440
+ target_schema: String,
441
+ staging: bool,
442
+ drain_threads: usize,
443
+ worker_threads: usize,
444
+ ) -> Result<Handle, Error> {
445
+ engine::check().map_err(|err| forked_handle_error(ruby, forked_while_running(&err)))?;
446
+ // Connecting can wait on the database (a staging worker installs its
447
+ // triggers before it reports ready), and `BlockingTrellis::connect` is a
448
+ // bare blocking wait, so it runs on a thread of its own while this Ruby
449
+ // thread waits for the result interruptibly. That is the one thread the
450
+ // binding starts for a wait, once per connect and never per call. An
451
+ // interrupted connect finishes there, and the engine it produces is
452
+ // dropped, which shuts it down (see `engine::Engine`'s `Drop`).
453
+ let (sender, reply) = handoff();
454
+ let spawned = std::thread::Builder::new()
455
+ .name("trellis-ruby-connect".to_string())
456
+ .spawn(move || {
457
+ let connected = catch_unwind(AssertUnwindSafe(|| {
458
+ let config = Config::with_schema(url, schema)?.with_target_schema(target_schema)?;
459
+ let options = TrellisOptions {
460
+ staging,
461
+ drain_threads,
462
+ worker_threads: Some(worker_threads),
463
+ ..Default::default()
464
+ };
465
+ // Can't fail after `check` passed (a process's answer never
466
+ // changes), but if it did, it's still an error rather than a
467
+ // hang.
468
+ Engine::connect(config, options)
469
+ .map_err(|err| {
470
+ PlainError::new(ErrorCode::Validation, forked_while_running(&err))
471
+ })?
472
+ .map_err(PlainError::from)
473
+ }))
474
+ .unwrap_or_else(|_| {
475
+ Err(PlainError::new(
476
+ ErrorCode::Internal,
477
+ "connecting a Trellis handle panicked",
478
+ ))
479
+ });
480
+ sender.send(connected);
481
+ });
482
+ if let Err(err) = spawned {
483
+ return Err(raise(
484
+ ruby,
485
+ PlainError::new(
486
+ ErrorCode::Internal,
487
+ format!("could not start a thread to connect the Trellis handle: {err}"),
488
+ ),
489
+ ));
490
+ }
491
+ let trellis = wait_without_gvl(reply)?.map_err(|err| raise(ruby, err))?;
492
+ Ok(Handle {
493
+ trellis: Arc::new(RwLock::new(Some(trellis))),
494
+ owner_pid: std::process::id(),
495
+ })
496
+ }
497
+
498
+ /// `Trellis::ForkedHandleError` with `message`.
499
+ fn forked_handle_error(ruby: &Ruby, message: String) -> Error {
500
+ match trellis_module(ruby)
501
+ .and_then(|module| module.const_get::<_, ExceptionClass>("ForkedHandleError"))
502
+ {
503
+ Ok(class) => Error::new(class, message),
504
+ Err(err) => err,
505
+ }
506
+ }
507
+
508
+ fn forked_while_running(err: &ForkedWhileRunning) -> String {
509
+ format!(
510
+ "this process ({}) was forked from process {} while that process had a Trellis engine \
511
+ running, so it may have inherited a lock one of the engine's threads held, which \
512
+ nothing in this process can release: it can't connect. Call \
513
+ Trellis::Instance.shutdown_all before forking and connect after: Puma's before_fork \
514
+ and before_worker_boot, and with fork_worker, before_worker_fork and \
515
+ after_worker_fork too (\"Forking servers\" in clients/ruby/README.md)",
516
+ std::process::id(),
517
+ err.parent
518
+ )
519
+ }
520
+
521
+ fn poisoned() -> PlainError {
522
+ PlainError::new(
523
+ ErrorCode::Internal,
524
+ "a call on this Trellis handle panicked; connect a new handle",
525
+ )
526
+ }
527
+
528
+ /// Waits for `future`'s reply on this Ruby thread with the GVL released, so
529
+ /// other Ruby threads run meanwhile, and with an unblocking function, so
530
+ /// `Thread#kill`, `Thread#raise` and Ctrl-C reach a thread that's waiting.
531
+ ///
532
+ /// No thread is started for the wait. The Ruby thread polls `future` with a
533
+ /// waker that signals a condition variable, then parks on it; the engine's
534
+ /// reply (through the waker) and Ruby's unblocking function both wake it. The
535
+ /// unblocking function only stops the wait: it never cancels the call, which
536
+ /// ends by its own 30-second deadline, and what it started on the server is
537
+ /// stopped by the server (issue #599). Dropping `future` on an interrupt
538
+ /// discards the reply.
539
+ ///
540
+ /// The outer `Err` is the interrupt's (an exception, or `Thread#kill`'s
541
+ /// jump), for Magnus to resume once this returns; the inner [`Reply`] is the
542
+ /// call's own result.
543
+ fn wait_without_gvl<F, T, E>(mut future: F) -> Result<Reply<T>, Error>
544
+ where
545
+ F: Future<Output = Result<T, E>> + Unpin,
546
+ E: Into<PlainError>,
547
+ {
548
+ let signal = Arc::new(Signal::new());
549
+ loop {
550
+ signal.rearm();
551
+ let mut waiting = Waiting {
552
+ future: &mut future,
553
+ signal: &signal,
554
+ output: None,
555
+ };
556
+ let data = std::ptr::from_mut(&mut waiting).cast::<c_void>();
557
+ let ubf_data = Arc::as_ptr(&signal).cast_mut().cast::<c_void>();
558
+ // SAFETY: `wait_for_reply::<F>` reads `data` as the `Waiting<F>` it
559
+ // points to, and `interrupt` reads `ubf_data` as the `Signal` it
560
+ // points to. `waiting` and `signal` outlive the call, and Ruby calls
561
+ // neither function once `rb_thread_call_without_gvl` has returned.
562
+ // Ruby may act on a pending interrupt (raise, or unwind for
563
+ // `Thread#kill`) before or after the wait; `protect` catches that
564
+ // jump, so it never unwinds through this frame, and returns it as an
565
+ // `Error` to resume later.
566
+ magnus::rb_sys::protect(|| unsafe {
567
+ rb_sys::rb_thread_call_without_gvl(
568
+ Some(wait_for_reply::<F>),
569
+ data,
570
+ Some(interrupt),
571
+ ubf_data,
572
+ );
573
+ rb_sys::Qnil as rb_sys::VALUE
574
+ })?;
575
+ match waiting.output {
576
+ Some(Ok(reply)) => return Ok(reply.map_err(Into::into)),
577
+ Some(Err(Panicked)) => {
578
+ return Ok(Err(PlainError::new(
579
+ ErrorCode::Internal,
580
+ "a Trellis call panicked; the handle may be unusable",
581
+ )));
582
+ }
583
+ None => {}
584
+ }
585
+ // Woken by the unblocking function with no reply yet. Let Ruby act
586
+ // on the interrupt: an exception or a kill leaves through `?`. One
587
+ // that turns out to raise nothing (a signal trap that returns, say)
588
+ // resumes the wait for the same reply.
589
+ //
590
+ // SAFETY: as above, `protect` keeps the jump out of this frame.
591
+ magnus::rb_sys::protect(|| unsafe {
592
+ rb_sys::rb_thread_check_ints();
593
+ rb_sys::Qnil as rb_sys::VALUE
594
+ })?;
595
+ }
596
+ }
597
+
598
+ /// What [`wait_for_reply`] works on, on the waiting Ruby thread's stack.
599
+ struct Waiting<'a, F: Future> {
600
+ future: &'a mut F,
601
+ signal: &'a Arc<Signal>,
602
+ /// Set once the future is ready (or panicked while polled).
603
+ output: Option<Result<F::Output, Panicked>>,
604
+ }
605
+
606
+ /// Polling the future panicked.
607
+ struct Panicked;
608
+
609
+ /// The wake-up state shared by the waiting thread, the future's waker, and
610
+ /// Ruby's unblocking function.
611
+ struct Signal {
612
+ flags: Mutex<Flags>,
613
+ changed: Condvar,
614
+ }
615
+
616
+ struct Flags {
617
+ /// Set by the waker: the future may be ready.
618
+ woken: bool,
619
+ /// Set by [`interrupt`], cleared by [`Signal::rearm`] before each wait.
620
+ interrupted: bool,
621
+ }
622
+
623
+ impl Signal {
624
+ fn new() -> Self {
625
+ Signal {
626
+ flags: Mutex::new(Flags {
627
+ woken: false,
628
+ interrupted: false,
629
+ }),
630
+ changed: Condvar::new(),
631
+ }
632
+ }
633
+
634
+ /// Never panics: the callbacks below run inside Ruby's C frames, where a
635
+ /// panic would abort the process. Nothing panics while holding the lock,
636
+ /// so a poisoned one still guards consistent flags.
637
+ fn lock(&self) -> MutexGuard<'_, Flags> {
638
+ self.flags.lock().unwrap_or_else(PoisonError::into_inner)
639
+ }
640
+
641
+ fn rearm(&self) {
642
+ let mut flags = self.lock();
643
+ flags.interrupted = false;
644
+ flags.woken = false;
645
+ }
646
+ }
647
+
648
+ impl Wake for Signal {
649
+ fn wake(self: Arc<Self>) {
650
+ self.wake_by_ref();
651
+ }
652
+
653
+ fn wake_by_ref(self: &Arc<Self>) {
654
+ self.lock().woken = true;
655
+ self.changed.notify_all();
656
+ }
657
+ }
658
+
659
+ /// The GVL-released half of [`wait_without_gvl`]: polls the future, and parks
660
+ /// until it is woken or [`interrupt`] fires, until the future is ready.
661
+ /// Touches no Ruby object.
662
+ unsafe extern "C" fn wait_for_reply<F: Future + Unpin>(data: *mut c_void) -> *mut c_void {
663
+ // SAFETY: see `wait_without_gvl`.
664
+ let waiting = unsafe { &mut *data.cast::<Waiting<'_, F>>() };
665
+ let waker = Waker::from(Arc::clone(waiting.signal));
666
+ let mut cx = Context::from_waker(&waker);
667
+ loop {
668
+ // A panic must not unwind into Ruby's C frames.
669
+ let polled = catch_unwind(AssertUnwindSafe(|| {
670
+ Pin::new(&mut *waiting.future).poll(&mut cx)
671
+ }));
672
+ match polled {
673
+ Ok(Poll::Ready(output)) => {
674
+ waiting.output = Some(Ok(output));
675
+ break;
676
+ }
677
+ Ok(Poll::Pending) => {}
678
+ Err(_) => {
679
+ waiting.output = Some(Err(Panicked));
680
+ break;
681
+ }
682
+ }
683
+ let mut flags = waiting.signal.lock();
684
+ while !flags.woken && !flags.interrupted {
685
+ flags = waiting
686
+ .signal
687
+ .changed
688
+ .wait(flags)
689
+ .unwrap_or_else(PoisonError::into_inner);
690
+ }
691
+ if flags.interrupted {
692
+ break;
693
+ }
694
+ flags.woken = false;
695
+ }
696
+ std::ptr::null_mut()
697
+ }
698
+
699
+ /// The unblocking function: Ruby calls it, from another thread, to wake a
700
+ /// thread in [`wait_for_reply`] that has an interrupt to handle. It takes a
701
+ /// lock, so it isn't async-signal-safe, and it doesn't claim to be
702
+ /// (`RB_NOGVL_UBF_ASYNC_SAFE`), so Ruby never calls it from a signal handler.
703
+ unsafe extern "C" fn interrupt(data: *mut c_void) {
704
+ // SAFETY: see `wait_without_gvl`.
705
+ let signal = unsafe { &*data.cast::<Signal>() };
706
+ signal.lock().interrupted = true;
707
+ signal.changed.notify_all();
708
+ }
709
+
710
+ /// A value one thread hands to another, as a future: the reply of the
711
+ /// thread [`connect`] starts.
712
+ struct Handoff<T> {
713
+ slot: Arc<Mutex<HandoffSlot<T>>>,
714
+ }
715
+
716
+ struct HandoffSlot<T> {
717
+ value: Option<Result<T, PlainError>>,
718
+ /// The sending end is gone.
719
+ closed: bool,
720
+ waker: Option<Waker>,
721
+ }
722
+
723
+ /// The sending end of a [`Handoff`]. Dropped without sending, the receiving
724
+ /// end reads an internal error rather than waiting forever.
725
+ struct HandoffSender<T> {
726
+ slot: Arc<Mutex<HandoffSlot<T>>>,
727
+ }
728
+
729
+ fn handoff<T>() -> (HandoffSender<T>, Handoff<T>) {
730
+ let slot = Arc::new(Mutex::new(HandoffSlot {
731
+ value: None,
732
+ closed: false,
733
+ waker: None,
734
+ }));
735
+ (
736
+ HandoffSender {
737
+ slot: Arc::clone(&slot),
738
+ },
739
+ Handoff { slot },
740
+ )
741
+ }
742
+
743
+ impl<T> HandoffSender<T> {
744
+ fn send(self, value: Result<T, PlainError>) {
745
+ self.slot
746
+ .lock()
747
+ .unwrap_or_else(PoisonError::into_inner)
748
+ .value = Some(value);
749
+ // The drop closes the slot and wakes the receiver.
750
+ }
751
+ }
752
+
753
+ impl<T> Drop for HandoffSender<T> {
754
+ fn drop(&mut self) {
755
+ let waker = {
756
+ let mut slot = self.slot.lock().unwrap_or_else(PoisonError::into_inner);
757
+ slot.closed = true;
758
+ slot.waker.take()
759
+ };
760
+ if let Some(waker) = waker {
761
+ waker.wake();
762
+ }
763
+ }
764
+ }
765
+
766
+ impl<T> Unpin for Handoff<T> {}
767
+
768
+ impl<T> Future for Handoff<T> {
769
+ type Output = Result<T, PlainError>;
770
+
771
+ fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
772
+ let mut slot = self.slot.lock().unwrap_or_else(PoisonError::into_inner);
773
+ if let Some(value) = slot.value.take() {
774
+ Poll::Ready(value)
775
+ } else if slot.closed {
776
+ Poll::Ready(Err(PlainError::new(
777
+ ErrorCode::Internal,
778
+ "the thread that was connecting a Trellis handle ended without a result",
779
+ )))
780
+ } else {
781
+ slot.waker = Some(cx.waker().clone());
782
+ Poll::Pending
783
+ }
784
+ }
785
+ }
786
+
787
+ fn trellis_module(ruby: &Ruby) -> Result<RModule, Error> {
788
+ ruby.class_object().const_get("Trellis")
789
+ }
790
+
791
+ /// `err` as the `Trellis::Error` subclass `Trellis::Error.from_native` picks
792
+ /// for its code.
793
+ fn raise(ruby: &Ruby, err: PlainError) -> Error {
794
+ let exception = trellis_module(ruby)
795
+ .and_then(|module| module.const_get::<_, RClass>("Error"))
796
+ .and_then(|class| class.funcall::<_, _, Exception>("from_native", (err.code, err.message)));
797
+ match exception {
798
+ Ok(exception) => exception.into(),
799
+ Err(err) => err,
800
+ }
801
+ }
802
+
803
+ /// `word`'s symbol. `word` is always one of the `'static` words
804
+ /// [`symbol_words`] lists, all interned by [`init`], never text read from
805
+ /// the database.
806
+ fn word_symbol(ruby: &Ruby, word: &'static str) -> StaticSymbol {
807
+ ruby.sym_new(word)
808
+ }
809
+
810
+ fn word_symbols(ruby: &Ruby, words: impl IntoIterator<Item = &'static str>) -> RArray {
811
+ ruby.ary_from_iter(words.into_iter().map(|word| word_symbol(ruby, word)))
812
+ }
813
+
814
+ /// Every word this extension turns into a symbol: the closed sets [`init`]
815
+ /// interns.
816
+ fn symbol_words() -> impl Iterator<Item = &'static str> {
817
+ transform_status_names()
818
+ .into_iter()
819
+ .chain(quarantine_state_names())
820
+ .chain(PlainApplied::KINDS)
821
+ .chain(SELF_CHECK_OUTCOMES)
822
+ .chain(SELF_CHECK_STATES)
823
+ .chain(SELF_CHECK_MODES)
824
+ .chain(DIVERGENCE_KINDS)
825
+ .chain(capture_failure_kind_names())
826
+ .chain(index_role_names())
827
+ }
828
+
829
+ /// A hash from symbol keys to `fields`' values: the shape every record
830
+ /// crosses as, for `lib/trellis/pg.rb` to turn into its `Data` value.
831
+ fn record<const N: usize>(ruby: &Ruby, fields: [(&str, Value); N]) -> Result<RHash, Error> {
832
+ let hash = ruby.hash_new();
833
+ for (name, value) in fields {
834
+ hash.aset(ruby.sym_new(name), value)?;
835
+ }
836
+ Ok(hash)
837
+ }
838
+
839
+ /// `items`, each turned into a Ruby value by `convert`.
840
+ fn array<T, V: IntoValue>(
841
+ ruby: &Ruby,
842
+ items: Vec<T>,
843
+ convert: impl Fn(&Ruby, T) -> Result<V, Error>,
844
+ ) -> Result<RArray, Error> {
845
+ let array = ruby.ary_new_capa(items.len());
846
+ for item in items {
847
+ array.push(convert(ruby, item)?)?;
848
+ }
849
+ Ok(array)
850
+ }
851
+
852
+ fn definition_summary_hash(ruby: &Ruby, summary: PlainDefinitionSummary) -> Result<RHash, Error> {
853
+ let failure = summary
854
+ .backfill_failure
855
+ .map(|failure| backfill_failure_hash(ruby, failure))
856
+ .transpose()?;
857
+ let halt = summary
858
+ .halt
859
+ .map(|halt| capture_failure_hash(ruby, halt))
860
+ .transpose()?;
861
+ record(
862
+ ruby,
863
+ [
864
+ ("target_table", ruby.into_value(summary.target_table)),
865
+ ("status", word_symbol(ruby, summary.status).as_value()),
866
+ (
867
+ "created_at_micros",
868
+ ruby.into_value(summary.created_at_micros),
869
+ ),
870
+ ("backfill_failure", ruby.into_value(failure)),
871
+ ("halt", ruby.into_value(halt)),
872
+ ],
873
+ )
874
+ }
875
+
876
+ fn status_hash(ruby: &Ruby, status: PlainDefinitionStatus) -> Result<RHash, Error> {
877
+ let failure = status
878
+ .backfill_failure
879
+ .map(|failure| backfill_failure_hash(ruby, failure))
880
+ .transpose()?;
881
+ let wait = status
882
+ .capture_wait
883
+ .map(|wait| capture_wait_hash(ruby, wait))
884
+ .transpose()?;
885
+ let capture_failure = status
886
+ .capture_failure
887
+ .map(|failure| capture_failure_hash(ruby, failure))
888
+ .transpose()?;
889
+ let held_keys = status
890
+ .held_keys
891
+ .map(|held| held_keys_hash(ruby, held))
892
+ .transpose()?;
893
+ let drain_failure = status
894
+ .drain_failure
895
+ .map(|failure| drain_failure_hash(ruby, failure))
896
+ .transpose()?;
897
+ let source_index_gaps = array(ruby, status.source_index_gaps, source_index_gap_hash)?;
898
+ let correcting = array(ruby, status.correcting, correcting_hash)?;
899
+ let paused_upstream_columns = ruby.ary_from_iter(status.paused_upstream_columns);
900
+ record(
901
+ ruby,
902
+ [
903
+ ("status", word_symbol(ruby, status.status).as_value()),
904
+ ("backfill_failure", ruby.into_value(failure)),
905
+ ("capture_wait", ruby.into_value(wait)),
906
+ ("capture_failure", ruby.into_value(capture_failure)),
907
+ ("held_keys", ruby.into_value(held_keys)),
908
+ ("drain_failure", ruby.into_value(drain_failure)),
909
+ ("source_index_gaps", source_index_gaps.as_value()),
910
+ ("correcting", correcting.as_value()),
911
+ (
912
+ "paused_upstream_columns",
913
+ paused_upstream_columns.as_value(),
914
+ ),
915
+ ],
916
+ )
917
+ }
918
+
919
+ /// A node of a partitioned source a definition is being corrected for.
920
+ fn correcting_hash(ruby: &Ruby, node: PlainCorrectingNode) -> Result<RHash, Error> {
921
+ record(
922
+ ruby,
923
+ [
924
+ ("node", ruby.into_value(node.node)),
925
+ ("since_micros", ruby.into_value(node.since_micros)),
926
+ ("keys_remaining", ruby.into_value(node.keys_remaining)),
927
+ ],
928
+ )
929
+ }
930
+
931
+ /// A source index a `GROUP BY` or `JOIN` target needs and lacks.
932
+ fn source_index_gap_hash(ruby: &Ruby, gap: PlainSourceIndexGap) -> Result<RHash, Error> {
933
+ record(
934
+ ruby,
935
+ [
936
+ ("table", ruby.into_value(gap.table)),
937
+ ("columns", ruby.into_value(gap.columns)),
938
+ ("role", word_symbol(ruby, gap.role).as_value()),
939
+ ("opted_out", ruby.into_value(gap.opted_out)),
940
+ ("fix", ruby.into_value(gap.fix)),
941
+ ],
942
+ )
943
+ }
944
+
945
+ /// A drain page that keeps failing with nothing charged or paused.
946
+ fn drain_failure_hash(ruby: &Ruby, failure: PlainDrainFailure) -> Result<RHash, Error> {
947
+ record(
948
+ ruby,
949
+ [
950
+ ("seg_seq", ruby.into_value(failure.seg_seq)),
951
+ ("tables", ruby.into_value(failure.tables)),
952
+ ("error", ruby.into_value(failure.error)),
953
+ ("sqlstate", ruby.into_value(failure.sqlstate)),
954
+ ("since_micros", ruby.into_value(failure.since_micros)),
955
+ (
956
+ "last_seen_micros",
957
+ ruby.into_value(failure.last_seen_micros),
958
+ ),
959
+ ("attempts", ruby.into_value(failure.attempts)),
960
+ ],
961
+ )
962
+ }
963
+
964
+ /// How many keys a definition holds in quarantine, and since when.
965
+ fn held_keys_hash(ruby: &Ruby, held: PlainHeldKeys) -> Result<RHash, Error> {
966
+ record(
967
+ ruby,
968
+ [
969
+ ("count", ruby.into_value(held.count)),
970
+ (
971
+ "oldest_poisoned_at_micros",
972
+ ruby.into_value(held.oldest_poisoned_at_micros),
973
+ ),
974
+ ],
975
+ )
976
+ }
977
+
978
+ fn capture_wait_hash(ruby: &Ruby, wait: PlainCaptureWait) -> Result<RHash, Error> {
979
+ record(
980
+ ruby,
981
+ [
982
+ ("table", ruby.into_value(wait.table)),
983
+ ("operation", ruby.into_value(wait.operation)),
984
+ ("lock_mode", ruby.into_value(wait.lock_mode)),
985
+ (
986
+ "waiting_since_micros",
987
+ ruby.into_value(wait.waiting_since_micros),
988
+ ),
989
+ (
990
+ "observed_at_micros",
991
+ ruby.into_value(wait.observed_at_micros),
992
+ ),
993
+ ("blockers", ruby.into_value(wait.blockers)),
994
+ ],
995
+ )
996
+ }
997
+
998
+ fn capture_failure_hash(ruby: &Ruby, failure: PlainCaptureFailure) -> Result<RHash, Error> {
999
+ record(
1000
+ ruby,
1001
+ [
1002
+ ("kind", word_symbol(ruby, failure.kind).as_value()),
1003
+ ("source_table", ruby.into_value(failure.source_table)),
1004
+ ("columns", ruby.into_value(failure.columns)),
1005
+ ("error", ruby.into_value(failure.error)),
1006
+ (
1007
+ "detected_at_micros",
1008
+ ruby.into_value(failure.detected_at_micros),
1009
+ ),
1010
+ ],
1011
+ )
1012
+ }
1013
+
1014
+ fn backfill_failure_hash(ruby: &Ruby, failure: PlainBackfillFailure) -> Result<RHash, Error> {
1015
+ record(
1016
+ ruby,
1017
+ [
1018
+ ("source_table", ruby.into_value(failure.source_table)),
1019
+ ("attempts", ruby.into_value(failure.attempts)),
1020
+ ("last_error", ruby.into_value(failure.last_error)),
1021
+ (
1022
+ "next_attempt_at_micros",
1023
+ ruby.into_value(failure.next_attempt_at_micros),
1024
+ ),
1025
+ ],
1026
+ )
1027
+ }
1028
+
1029
+ /// What `apply` did. `kind` is one of [`PlainApplied::KINDS`]; each other
1030
+ /// field is set only for the kinds that carry it (`target_table` for
1031
+ /// `transform_defined` and `altered`, `columns` for `resumed`, and `added`, `dropped`
1032
+ /// and `altered` for `altered`), and is `nil` otherwise: the same flat shape
1033
+ /// the Elixir NIF hands `Trellis.Applied`.
1034
+ fn applied_hash(ruby: &Ruby, applied: PlainApplied) -> Result<RHash, Error> {
1035
+ let kind = word_symbol(ruby, applied.kind()).as_value();
1036
+ let nil = ruby.qnil().as_value();
1037
+ let (mut target_table, mut columns) = (nil, nil);
1038
+ let (mut added, mut dropped, mut altered) = (nil, nil, nil);
1039
+ match applied {
1040
+ PlainApplied::TransformDefined(plain) => target_table = ruby.into_value(plain),
1041
+ PlainApplied::Resumed { columns: resumed } => columns = ruby.into_value(resumed),
1042
+ PlainApplied::Altered {
1043
+ target_table: plain,
1044
+ added: plain_added,
1045
+ dropped: plain_dropped,
1046
+ altered: plain_altered,
1047
+ } => {
1048
+ target_table = ruby.into_value(plain);
1049
+ added = ruby.into_value(plain_added);
1050
+ dropped = ruby.into_value(plain_dropped);
1051
+ altered = ruby.into_value(plain_altered);
1052
+ }
1053
+ PlainApplied::Paused | PlainApplied::Dropped | PlainApplied::Unknown => {}
1054
+ }
1055
+ record(
1056
+ ruby,
1057
+ [
1058
+ ("kind", kind),
1059
+ ("target_table", target_table),
1060
+ ("columns", columns),
1061
+ ("added", added),
1062
+ ("dropped", dropped),
1063
+ ("altered", altered),
1064
+ ],
1065
+ )
1066
+ }
1067
+
1068
+ fn quarantine_entry_hash(ruby: &Ruby, entry: PlainQuarantineEntry) -> Result<RHash, Error> {
1069
+ record(
1070
+ ruby,
1071
+ [
1072
+ ("target", ruby.into_value(entry.target)),
1073
+ ("state", word_symbol(ruby, entry.state).as_value()),
1074
+ ("paused_at_micros", ruby.into_value(entry.paused_at_micros)),
1075
+ ("last_error", ruby.into_value(entry.last_error)),
1076
+ ],
1077
+ )
1078
+ }
1079
+
1080
+ fn poison_entry_hash(ruby: &Ruby, entry: PlainPoisonEntry) -> Result<RHash, Error> {
1081
+ record(
1082
+ ruby,
1083
+ [
1084
+ ("transform", ruby.into_value(entry.transform)),
1085
+ ("src_table", ruby.into_value(entry.src_table)),
1086
+ ("key", ruby.into_value(entry.key)),
1087
+ ("last_error", ruby.into_value(entry.last_error)),
1088
+ (
1089
+ "poisoned_at_micros",
1090
+ ruby.into_value(entry.poisoned_at_micros),
1091
+ ),
1092
+ ],
1093
+ )
1094
+ }
1095
+
1096
+ /// A `self_check` job. `mode` is one of [`SELF_CHECK_MODES`], `state` one of
1097
+ /// [`SELF_CHECK_STATES`]; `report` is set once it is `done`.
1098
+ fn self_check_job_hash(ruby: &Ruby, job: PlainSelfCheckJob) -> Result<RHash, Error> {
1099
+ let report = job
1100
+ .report
1101
+ .map(|report| self_check_hash(ruby, report))
1102
+ .transpose()?;
1103
+ record(
1104
+ ruby,
1105
+ [
1106
+ ("id", ruby.into_value(job.id)),
1107
+ ("target", ruby.into_value(job.target)),
1108
+ ("mode", word_symbol(ruby, job.mode).as_value()),
1109
+ ("state", word_symbol(ruby, job.state).as_value()),
1110
+ ("rows_compared", ruby.into_value(job.rows_compared)),
1111
+ ("report", ruby.into_value(report)),
1112
+ ("error", ruby.into_value(job.error)),
1113
+ ],
1114
+ )
1115
+ }
1116
+
1117
+ /// What a finished `self_check` job found. `outcome` is one of [`SELF_CHECK_OUTCOMES`];
1118
+ /// `divergences` is empty unless it is `diverged`, and `status` is set only
1119
+ /// when it is `not_live`.
1120
+ fn self_check_hash(ruby: &Ruby, report: PlainSelfCheckReport) -> Result<RHash, Error> {
1121
+ let divergences = array(ruby, report.divergences, divergence_hash)?;
1122
+ let held_keys = report
1123
+ .held_keys
1124
+ .map(|held| held_keys_hash(ruby, held))
1125
+ .transpose()?;
1126
+ let drain_failures = array(ruby, report.drain_failures, drain_failure_hash)?;
1127
+ let status = report
1128
+ .status
1129
+ .map(|status| word_symbol(ruby, status).as_value());
1130
+ record(
1131
+ ruby,
1132
+ [
1133
+ ("target", ruby.into_value(report.target)),
1134
+ ("checked_through", ruby.into_value(report.checked_through)),
1135
+ ("rows_compared", ruby.into_value(report.rows_compared)),
1136
+ ("truncated", ruby.into_value(report.truncated)),
1137
+ ("outcome", word_symbol(ruby, report.outcome).as_value()),
1138
+ ("status", ruby.into_value(status)),
1139
+ ("divergences", divergences.as_value()),
1140
+ ("held_keys", ruby.into_value(held_keys)),
1141
+ ("drain_failures", drain_failures.as_value()),
1142
+ ],
1143
+ )
1144
+ }
1145
+
1146
+ /// One divergence. `kind` is one of [`DIVERGENCE_KINDS`]; see
1147
+ /// [`PlainDivergence`] for which other fields each kind sets.
1148
+ fn divergence_hash(ruby: &Ruby, divergence: PlainDivergence) -> Result<RHash, Error> {
1149
+ record(
1150
+ ruby,
1151
+ [
1152
+ ("kind", word_symbol(ruby, divergence.kind).as_value()),
1153
+ ("key", ruby.into_value(divergence.key)),
1154
+ ("column", ruby.into_value(divergence.column)),
1155
+ ("persisted", ruby.into_value(divergence.persisted)),
1156
+ ("recomputed", ruby.into_value(divergence.recomputed)),
1157
+ ("table", ruby.into_value(divergence.table)),
1158
+ ("detail", ruby.into_value(divergence.detail)),
1159
+ ],
1160
+ )
1161
+ }
1162
+
1163
+ /// Every error code `trellis-embed` maps explicitly, for the test asserting
1164
+ /// each one has its own `Trellis::Error` subclass.
1165
+ fn error_codes() -> Vec<&'static str> {
1166
+ ERROR_CODES.to_vec()
1167
+ }
1168
+
1169
+ /// Every status symbol `define`, `status` and `definitions` can return.
1170
+ fn status_names(ruby: &Ruby) -> RArray {
1171
+ word_symbols(ruby, transform_status_names())
1172
+ }
1173
+
1174
+ /// Every state symbol `quarantined` and `quarantine_status` can return.
1175
+ fn quarantine_states(ruby: &Ruby) -> RArray {
1176
+ word_symbols(ruby, quarantine_state_names())
1177
+ }
1178
+
1179
+ /// Every outcome kind `apply`'s result can carry.
1180
+ fn applied_kinds(ruby: &Ruby) -> RArray {
1181
+ word_symbols(ruby, PlainApplied::KINDS)
1182
+ }
1183
+
1184
+ /// Every outcome symbol `self_check`'s report can carry.
1185
+ fn self_check_outcomes(ruby: &Ruby) -> RArray {
1186
+ word_symbols(ruby, SELF_CHECK_OUTCOMES)
1187
+ }
1188
+
1189
+ /// Every mode symbol a `self_check` job can carry.
1190
+ fn self_check_modes(ruby: &Ruby) -> RArray {
1191
+ word_symbols(ruby, SELF_CHECK_MODES)
1192
+ }
1193
+
1194
+ /// Every state symbol a `self_check` job can carry.
1195
+ fn self_check_states(ruby: &Ruby) -> RArray {
1196
+ word_symbols(ruby, SELF_CHECK_STATES)
1197
+ }
1198
+
1199
+ /// Every divergence kind symbol `self_check`'s report can carry.
1200
+ fn divergence_kinds(ruby: &Ruby) -> RArray {
1201
+ word_symbols(ruby, DIVERGENCE_KINDS)
1202
+ }
1203
+
1204
+ /// Every kind symbol a capture failure can carry.
1205
+ fn capture_failure_kinds(ruby: &Ruby) -> RArray {
1206
+ word_symbols(ruby, capture_failure_kind_names())
1207
+ }
1208
+
1209
+ /// Every role symbol a source index gap can carry.
1210
+ fn index_roles(ruby: &Ruby) -> RArray {
1211
+ word_symbols(ruby, index_role_names())
1212
+ }
1213
+
1214
+ /// Every statement kind `trellis`'s grammar has, for the test asserting
1215
+ /// `apply`'s round trip covers each one.
1216
+ fn statement_kinds() -> Vec<&'static str> {
1217
+ trellis::StatementKind::ALL
1218
+ .iter()
1219
+ .map(|kind| kind.as_str())
1220
+ .collect()
1221
+ }
1222
+
1223
+ #[magnus::init]
1224
+ fn init(ruby: &Ruby) -> Result<(), Error> {
1225
+ // Interns every symbol a result can carry up front, from the closed
1226
+ // sets, so encoding a result never creates one.
1227
+ for word in symbol_words() {
1228
+ word_symbol(ruby, word);
1229
+ }
1230
+
1231
+ let native = ruby.define_module("Trellis")?.define_module("Native")?;
1232
+ native.define_module_function("connect", function!(connect, 6))?;
1233
+ native.define_module_function("error_codes", function!(error_codes, 0))?;
1234
+ native.define_module_function("status_names", function!(status_names, 0))?;
1235
+ native.define_module_function("quarantine_states", function!(quarantine_states, 0))?;
1236
+ native.define_module_function("applied_kinds", function!(applied_kinds, 0))?;
1237
+ native.define_module_function("self_check_outcomes", function!(self_check_outcomes, 0))?;
1238
+ native.define_module_function("self_check_modes", function!(self_check_modes, 0))?;
1239
+ native.define_module_function("self_check_states", function!(self_check_states, 0))?;
1240
+ native.define_module_function("divergence_kinds", function!(divergence_kinds, 0))?;
1241
+ native.define_module_function("capture_failure_kinds", function!(capture_failure_kinds, 0))?;
1242
+ native.define_module_function("index_roles", function!(index_roles, 0))?;
1243
+ native.define_module_function("statement_kinds", function!(statement_kinds, 0))?;
1244
+
1245
+ let handle = native.define_class("Handle", ruby.class_object())?;
1246
+ handle.define_method("owner_pid", method!(Handle::owner_pid, 0))?;
1247
+ handle.define_method("migrate", method!(Handle::migrate, 0))?;
1248
+ handle.define_method("define", method!(Handle::define, 1))?;
1249
+ handle.define_method("apply", method!(Handle::apply, 1))?;
1250
+ handle.define_method("status", method!(Handle::status, 1))?;
1251
+ handle.define_method("definitions", method!(Handle::definitions, 0))?;
1252
+ handle.define_method(
1253
+ "dump_definitions_sql",
1254
+ method!(Handle::dump_definitions_sql, 0),
1255
+ )?;
1256
+ handle.define_method("request_backfill", method!(Handle::request_backfill, 1))?;
1257
+ handle.define_method("release_key", method!(Handle::release_key, 3))?;
1258
+ handle.define_method("poisoned_since", method!(Handle::poisoned_since, 1))?;
1259
+ handle.define_method("quarantined", method!(Handle::quarantined, 0))?;
1260
+ handle.define_method("quarantine_status", method!(Handle::quarantine_status, 1))?;
1261
+ handle.define_method("sample_quarantined", method!(Handle::sample_quarantined, 3))?;
1262
+ handle.define_method(
1263
+ "has_live_drain_workers",
1264
+ method!(Handle::has_live_drain_workers, 0),
1265
+ )?;
1266
+ handle.define_method(
1267
+ "detect_capture_disruptions",
1268
+ method!(Handle::detect_capture_disruptions, 0),
1269
+ )?;
1270
+ handle.define_method(
1271
+ "has_live_staging_worker",
1272
+ method!(Handle::has_live_staging_worker, 0),
1273
+ )?;
1274
+ handle.define_method("watermark_token", method!(Handle::watermark_token, 0))?;
1275
+ handle.define_method("await_converged", method!(Handle::await_converged, 2))?;
1276
+ handle.define_method("self_check", method!(Handle::self_check, 3))?;
1277
+ handle.define_method("self_check_job", method!(Handle::self_check_job, 1))?;
1278
+ handle.define_method("config", method!(Handle::config, 0))?;
1279
+ handle.define_method("shutdown", method!(Handle::shutdown, 0))?;
1280
+ Ok(())
1281
+ }