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,37 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "trellis_ruby"
|
|
3
|
+
version = "0.1.0-alpha"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
publish = false
|
|
6
|
+
|
|
7
|
+
# The Ruby binding's native extension (docs/decisions/0010-embeddable-clients.md):
|
|
8
|
+
# a thin Magnus wrapper over `trellis::BlockingTrellis`. Plain-data flattening
|
|
9
|
+
# and error codes live in `trellis-embed`, shared with the Elixir binding; this
|
|
10
|
+
# crate only turns those plain values into Ruby objects.
|
|
11
|
+
[lib]
|
|
12
|
+
crate-type = ["cdylib"]
|
|
13
|
+
# No Rust tests live here: the conversions are `trellis-embed`'s and tested
|
|
14
|
+
# there, and everything else needs a Ruby VM, so it is tested by `rake test`.
|
|
15
|
+
test = false
|
|
16
|
+
doctest = false
|
|
17
|
+
|
|
18
|
+
# Unlike Rustler, `rb-sys` can't build without Ruby: its build script asks a
|
|
19
|
+
# Ruby install for its headers and link flags, and generates bindings with
|
|
20
|
+
# libclang. The Rust CI job and `verify` have neither, and still build this
|
|
21
|
+
# crate (`cargo test --workspace`), so everything in it sits behind `ruby`,
|
|
22
|
+
# which is off by default: without it the crate is an empty library and
|
|
23
|
+
# `rb-sys` isn't built at all. The root manifest leaves the crate out of
|
|
24
|
+
# `default-members`, so `--all-features` without `--workspace` doesn't turn
|
|
25
|
+
# it on either. `rake compile` (clients/ruby/Rakefile) turns it on, and the
|
|
26
|
+
# `ruby` CI job lints it the same way.
|
|
27
|
+
[features]
|
|
28
|
+
ruby = ["dep:magnus", "dep:rb-sys"]
|
|
29
|
+
|
|
30
|
+
[dependencies]
|
|
31
|
+
trellis = { git = "https://github.com/salesforce-misc/trellis", rev = "31985aab1f7ddb22513973b0e85882c3eca9583f" }
|
|
32
|
+
trellis-embed = { git = "https://github.com/salesforce-misc/trellis", rev = "31985aab1f7ddb22513973b0e85882c3eca9583f" }
|
|
33
|
+
magnus = { version = "0.9", optional = true, default-features = false, features = ["rb-sys"] }
|
|
34
|
+
rb-sys = { version = "0.9", optional = true }
|
|
35
|
+
|
|
36
|
+
# Its own workspace root, not part of any Cargo workspace it is installed under.
|
|
37
|
+
[workspace]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Builds the native extension through Cargo with rb-sys: what installing the
|
|
4
|
+
# source gem runs, and what `rake compile` and the cross-compiled platform
|
|
5
|
+
# gems run too (see the Rakefile).
|
|
6
|
+
#
|
|
7
|
+
# The `ruby` feature is the whole extension (see Cargo.toml). The `trellis`
|
|
8
|
+
# crate's optional `otlp` feature stays off: nothing in the binding uses it,
|
|
9
|
+
# and it would pull in an OpenTelemetry and gRPC dependency tree.
|
|
10
|
+
require "mkmf"
|
|
11
|
+
require "rb_sys/mkmf"
|
|
12
|
+
|
|
13
|
+
create_rust_makefile("trellis/trellis_ruby") do |r|
|
|
14
|
+
r.features = ["ruby"]
|
|
15
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
//! Knowing whether a forked child may connect (issue #600).
|
|
2
|
+
//!
|
|
3
|
+
//! `fork()` copies the whole address space but only the calling thread. The
|
|
4
|
+
//! engine keeps process-wide state behind locks (the enum type-name
|
|
5
|
+
//! interner, the metrics registry, and others inside its dependencies:
|
|
6
|
+
//! `tracing`, `quanta`, Rust's stdout), and its threads take them while they
|
|
7
|
+
//! work. A lock one of them held at the moment of a fork is copied held, and
|
|
8
|
+
//! no thread in the child will ever release it, so a child that connects an
|
|
9
|
+
//! engine of its own can wait on it forever. Nothing the child does after
|
|
10
|
+
//! the fork can undo that. The only sound rule is the one this module
|
|
11
|
+
//! enforces: a child may connect only if its parent had no engine thread
|
|
12
|
+
//! running when it forked.
|
|
13
|
+
//!
|
|
14
|
+
//! [`ENGINES`] counts this process's engines from before an engine's first
|
|
15
|
+
//! thread starts until after its last one has exited. A forked child
|
|
16
|
+
//! inherits the count along with the rest of memory, so the child's first
|
|
17
|
+
//! `connect` can tell, from the count alone, whether its parent had an
|
|
18
|
+
//! engine running at the fork, whatever did the forking (`Kernel#fork`, a C
|
|
19
|
+
//! extension's `fork()`, a server's worker spawner). It needs no fork hook.
|
|
20
|
+
//!
|
|
21
|
+
//! "Running" is deliberately broader than "held by `Trellis`": an engine
|
|
22
|
+
//! still connecting on another thread, or still winding down after an
|
|
23
|
+
//! interrupted `shutdown` or a dropped handle, has threads too, and counts.
|
|
24
|
+
|
|
25
|
+
use std::future::Future;
|
|
26
|
+
use std::ops::Deref;
|
|
27
|
+
use std::pin::Pin;
|
|
28
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
29
|
+
use std::task::{Context, Poll, ready};
|
|
30
|
+
|
|
31
|
+
use trellis::{BlockingTrellis, Config, Shutdown, TrellisError, TrellisOptions};
|
|
32
|
+
|
|
33
|
+
/// The process whose engines are counted (high 32 bits: its pid) and how many
|
|
34
|
+
/// are running (low 32 bits). One word, so a count and its owner change
|
|
35
|
+
/// together. A count whose owner isn't this process was inherited through
|
|
36
|
+
/// `fork`, and can never drop: the threads it counts don't exist here.
|
|
37
|
+
static ENGINES: AtomicU64 = AtomicU64::new(0);
|
|
38
|
+
|
|
39
|
+
fn pack(pid: u32, count: u32) -> u64 {
|
|
40
|
+
(u64::from(pid) << 32) | u64::from(count)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn unpack(word: u64) -> (u32, u32) {
|
|
44
|
+
((word >> 32) as u32, word as u32)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Why this process can't connect: it was forked from `parent` while
|
|
48
|
+
/// `parent` had an engine running.
|
|
49
|
+
pub(crate) struct ForkedWhileRunning {
|
|
50
|
+
pub(crate) parent: u32,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Whether this process may start an engine. Exact, not a snapshot that can
|
|
54
|
+
/// go stale: a process forked while its parent's engine ran stays that way
|
|
55
|
+
/// for good, since nothing here ever lowers an inherited count.
|
|
56
|
+
pub(crate) fn check() -> Result<(), ForkedWhileRunning> {
|
|
57
|
+
let (owner, count) = unpack(ENGINES.load(Ordering::SeqCst));
|
|
58
|
+
if owner != std::process::id() && count > 0 {
|
|
59
|
+
return Err(ForkedWhileRunning { parent: owner });
|
|
60
|
+
}
|
|
61
|
+
Ok(())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// One count in [`ENGINES`]. There is no `Drop`: a lease that is lost rather
|
|
65
|
+
/// than [released](Lease::release) keeps its count for good, which only
|
|
66
|
+
/// ever makes a later child refuse to connect, never lets one connect when
|
|
67
|
+
/// it shouldn't.
|
|
68
|
+
struct Lease(());
|
|
69
|
+
|
|
70
|
+
impl Lease {
|
|
71
|
+
fn take() -> Result<Lease, ForkedWhileRunning> {
|
|
72
|
+
let me = std::process::id();
|
|
73
|
+
let mut word = ENGINES.load(Ordering::SeqCst);
|
|
74
|
+
loop {
|
|
75
|
+
let (owner, count) = unpack(word);
|
|
76
|
+
let next = if owner == me {
|
|
77
|
+
count + 1
|
|
78
|
+
} else if count == 0 {
|
|
79
|
+
1
|
|
80
|
+
} else {
|
|
81
|
+
return Err(ForkedWhileRunning { parent: owner });
|
|
82
|
+
};
|
|
83
|
+
match ENGINES.compare_exchange(word, pack(me, next), Ordering::SeqCst, Ordering::SeqCst)
|
|
84
|
+
{
|
|
85
|
+
Ok(_) => return Ok(Lease(())),
|
|
86
|
+
Err(seen) => word = seen,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Only once every thread the engine started has exited.
|
|
92
|
+
fn release(self) {
|
|
93
|
+
ENGINES.fetch_sub(1, Ordering::SeqCst);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// A [`BlockingTrellis`] counted in [`ENGINES`] for as long as it may have
|
|
98
|
+
/// threads.
|
|
99
|
+
pub(crate) struct Engine {
|
|
100
|
+
/// `None` only once `shutdown` or `drop` has taken it.
|
|
101
|
+
running: Option<(BlockingTrellis, Lease)>,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl Engine {
|
|
105
|
+
/// Connects, counting the engine before its first thread starts. The
|
|
106
|
+
/// outer `Err` is [`check`]'s refusal, before anything has started.
|
|
107
|
+
pub(crate) fn connect(
|
|
108
|
+
config: Config,
|
|
109
|
+
options: TrellisOptions,
|
|
110
|
+
) -> Result<Result<Engine, TrellisError>, ForkedWhileRunning> {
|
|
111
|
+
let lease = Lease::take()?;
|
|
112
|
+
Ok(match BlockingTrellis::connect(config, options) {
|
|
113
|
+
Ok(trellis) => Ok(Engine {
|
|
114
|
+
running: Some((trellis, lease)),
|
|
115
|
+
}),
|
|
116
|
+
Err(err) => {
|
|
117
|
+
// A failed connect has joined every thread it started.
|
|
118
|
+
lease.release();
|
|
119
|
+
Err(err)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Starts stopping the engine, returning the wait for its threads to end.
|
|
125
|
+
/// The wait is a future, so the caller can wait interruptibly; if it is
|
|
126
|
+
/// dropped before it finishes, the shutdown is finished on a thread of
|
|
127
|
+
/// its own (see [`Stopping`]).
|
|
128
|
+
pub(crate) fn start_shutdown(mut self) -> Stopping {
|
|
129
|
+
let (trellis, lease) = self
|
|
130
|
+
.running
|
|
131
|
+
.take()
|
|
132
|
+
.expect("an engine is shut down only once");
|
|
133
|
+
Stopping {
|
|
134
|
+
running: Some((trellis.start_shutdown(), lease)),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// An engine's shutdown under way, ready once its threads have exited and
|
|
140
|
+
/// its count is given back.
|
|
141
|
+
pub(crate) struct Stopping {
|
|
142
|
+
/// `None` once the shutdown has finished.
|
|
143
|
+
running: Option<(Shutdown, Lease)>,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Gives `lease` back once `result` shows the engine's threads are gone.
|
|
147
|
+
/// `BlockingThreadGone` means its background thread panicked, which drops the
|
|
148
|
+
/// engine without joining the threads it started, so that count is kept.
|
|
149
|
+
fn finish(result: &Result<(), TrellisError>, lease: Lease) {
|
|
150
|
+
if !matches!(result, Err(TrellisError::BlockingThreadGone)) {
|
|
151
|
+
lease.release();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
impl Future for Stopping {
|
|
156
|
+
type Output = Result<(), TrellisError>;
|
|
157
|
+
|
|
158
|
+
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
159
|
+
let Some((shutdown, _)) = self.running.as_mut() else {
|
|
160
|
+
return Poll::Ready(Ok(()));
|
|
161
|
+
};
|
|
162
|
+
let result = ready!(Pin::new(shutdown).poll(cx));
|
|
163
|
+
if let Some((_, lease)) = self.running.take() {
|
|
164
|
+
finish(&result, lease);
|
|
165
|
+
}
|
|
166
|
+
Poll::Ready(result)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
impl Drop for Stopping {
|
|
171
|
+
/// A shutdown abandoned before it finished (its waiter was interrupted)
|
|
172
|
+
/// still runs to its end: a thread of its own waits for it, so the engine
|
|
173
|
+
/// is joined and its count given back once its threads are gone. This is
|
|
174
|
+
/// the one thread the binding starts on a caller's behalf, and only for
|
|
175
|
+
/// an interrupted shutdown, never for a call.
|
|
176
|
+
fn drop(&mut self) {
|
|
177
|
+
if let Some((shutdown, lease)) = self.running.take() {
|
|
178
|
+
// If the thread can't start, the closure (and the lease in it) is
|
|
179
|
+
// dropped unreleased: the engine winds down unjoined, and its
|
|
180
|
+
// count stays.
|
|
181
|
+
let _ = std::thread::Builder::new()
|
|
182
|
+
.name("trellis-ruby-stop".to_string())
|
|
183
|
+
.spawn(move || finish(&shutdown.wait(), lease));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// Shuts `trellis` down and gives its count back once its threads are gone.
|
|
189
|
+
fn stop(trellis: BlockingTrellis, lease: Lease) -> Result<(), TrellisError> {
|
|
190
|
+
let result = trellis.shutdown();
|
|
191
|
+
finish(&result, lease);
|
|
192
|
+
result
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
impl Deref for Engine {
|
|
196
|
+
type Target = BlockingTrellis;
|
|
197
|
+
|
|
198
|
+
fn deref(&self) -> &BlockingTrellis {
|
|
199
|
+
&self.running.as_ref().expect("a live engine").0
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
impl Drop for Engine {
|
|
204
|
+
/// An engine dropped without `shutdown` (a handle garbage-collected while
|
|
205
|
+
/// connected, or a `connect` whose caller was interrupted and gave up on
|
|
206
|
+
/// it) is shut down on a thread of its own, so the thread dropping it
|
|
207
|
+
/// (Ruby's GC, say) doesn't wait, and its count is still given back only
|
|
208
|
+
/// once its threads are gone. Never reached in a forked child: `Handle`
|
|
209
|
+
/// leaks an inherited engine rather than dropping it.
|
|
210
|
+
fn drop(&mut self) {
|
|
211
|
+
if let Some((trellis, lease)) = self.running.take() {
|
|
212
|
+
// If the thread can't start, the closure (and the lease in it) is
|
|
213
|
+
// dropped unreleased: the engine winds down unjoined, and its
|
|
214
|
+
// count stays.
|
|
215
|
+
let _ = std::thread::Builder::new()
|
|
216
|
+
.name("trellis-ruby-stop".to_string())
|
|
217
|
+
.spawn(move || {
|
|
218
|
+
let _ = stop(trellis, lease);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|