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.
- checksums.yaml +7 -0
- data/LICENSE.txt +206 -0
- data/README.md +567 -0
- data/ext/trellis_ruby/Cargo.lock +2218 -0
- data/ext/trellis_ruby/Cargo.toml +37 -0
- data/ext/trellis_ruby/extconf.rb +15 -0
- data/ext/trellis_ruby/src/engine.rs +222 -0
- data/ext/trellis_ruby/src/lib.rs +1281 -0
- data/lib/trellis/error.rb +103 -0
- data/lib/trellis/instance.rb +540 -0
- data/lib/trellis/migration.rb +186 -0
- data/lib/trellis/pg.rb +140 -0
- data/lib/trellis/railtie.rb +293 -0
- data/lib/trellis/schema_dump.rb +278 -0
- data/lib/trellis/test_support.rb +161 -0
- data/lib/trellis/values.rb +386 -0
- data/lib/trellis/version.rb +7 -0
- metadata +77 -0
|
@@ -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
|
+
}
|