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,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
+ }