itsi-server 0.1.1 → 0.1.2

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/exe/itsi +88 -28
  3. data/ext/itsi_error/Cargo.toml +2 -0
  4. data/ext/itsi_error/src/from.rs +70 -0
  5. data/ext/itsi_error/src/lib.rs +10 -37
  6. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  7. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  8. data/ext/itsi_rb_helpers/Cargo.toml +2 -0
  9. data/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
  10. data/ext/itsi_rb_helpers/src/lib.rs +90 -10
  11. data/ext/itsi_scheduler/Cargo.toml +24 -0
  12. data/ext/itsi_scheduler/extconf.rb +6 -0
  13. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  14. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  15. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  16. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  17. data/ext/itsi_scheduler/src/lib.rs +38 -0
  18. data/ext/itsi_server/Cargo.toml +14 -2
  19. data/ext/itsi_server/extconf.rb +1 -1
  20. data/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  21. data/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  22. data/ext/itsi_server/src/body_proxy/mod.rs +2 -0
  23. data/ext/itsi_server/src/lib.rs +58 -7
  24. data/ext/itsi_server/src/request/itsi_request.rs +238 -104
  25. data/ext/itsi_server/src/response/itsi_response.rs +347 -0
  26. data/ext/itsi_server/src/response/mod.rs +1 -0
  27. data/ext/itsi_server/src/server/bind.rs +50 -20
  28. data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  29. data/ext/itsi_server/src/server/io_stream.rs +104 -0
  30. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +11 -30
  31. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +3 -50
  32. data/ext/itsi_server/src/server/itsi_server.rs +181 -133
  33. data/ext/itsi_server/src/server/lifecycle_event.rs +8 -0
  34. data/ext/itsi_server/src/server/listener.rs +169 -128
  35. data/ext/itsi_server/src/server/mod.rs +7 -1
  36. data/ext/itsi_server/src/server/process_worker.rs +196 -0
  37. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +253 -0
  38. data/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  39. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +238 -0
  40. data/ext/itsi_server/src/server/signal.rs +57 -0
  41. data/ext/itsi_server/src/server/thread_worker.rs +368 -0
  42. data/ext/itsi_server/src/server/tls.rs +42 -28
  43. data/ext/itsi_tracing/Cargo.toml +4 -0
  44. data/ext/itsi_tracing/src/lib.rs +36 -6
  45. data/lib/itsi/request.rb +30 -14
  46. data/lib/itsi/server/rack/handler/itsi.rb +25 -0
  47. data/lib/itsi/server/scheduler_mode.rb +6 -0
  48. data/lib/itsi/server/version.rb +1 -1
  49. data/lib/itsi/server.rb +68 -2
  50. data/lib/itsi/signals.rb +18 -0
  51. data/lib/itsi/stream_io.rb +38 -0
  52. metadata +41 -14
  53. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  54. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a9e031c2f6664022551a7580e10114137b7c75636321d9a52fbb7cefc60d528
4
- data.tar.gz: 292f8b45bb17d0b58ce4c9a6d440576f8fa801d1b45b44a59039a53b4ea4ab07
3
+ metadata.gz: a59f65a9b7f82d2e3e4e94eed7b1de48b9579e6a2ee40887f6de7500da9cb391
4
+ data.tar.gz: 7e49394fbbef18255d4a15ac6335b7acb74d925f2c026c5b24dcf9032f37bdc9
5
5
  SHA512:
6
- metadata.gz: 719ce5de81a4fb823dab949a5fc0890ebeec5e94578a92440d8233e33fd3f6756c804511592e667db88764ef172ca2f67028cd2a8aa2a645608ae4d0ee11187f
7
- data.tar.gz: 4f71c1610cfc6f92d05f56c84783dc7ddc701ff108fc5f24ee83820de52fe0eb0899de3bbcf38ca5326efbe4f20d344a022c3656136430869d9085d2d56ef9d2
6
+ metadata.gz: 5886b25965a3fa73fb3ad3f6d7e915558f5cbf69a6dd62ab8dee23b51d64735b43d90942eadb05b44cb374cd64e00ad6eec078686511ca257f5b3f3b3fe99d48
7
+ data.tar.gz: b799d8fe05a30559d593f14b13c8b9c9a9f9569b610703e904177ca177671f171e5493615a48c39dd181fb9c2a67f75945e8c592d84ae15414ca4ebef3929ed4
data/exe/itsi CHANGED
@@ -1,27 +1,32 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
-
4
3
  require "optparse"
5
- require "rack"
6
- require "etc"
7
4
 
8
5
  # Default options used when starting Osprey from the CLI using `osprey`
9
6
  DEFAULT_OPTIONS = {
10
7
  # Number of workers
11
- workers: Etc.nprocessors,
8
+ workers: 1,
12
9
  # Number of threads per worker
13
10
  threads: 1,
14
11
  # Graceful shutdown timeout
15
- shutdown_timeout: 0.3,
12
+ shutdown_timeout: 5,
16
13
  # Binds
17
- binds: ['http://0.0.0.0:3000']
14
+ binds: ["http://0.0.0.0:3000"],
15
+ # Preload
16
+ preload: true,
17
+ # Rackup file
18
+ rackup_file: "config.ru",
19
+ # Scheduler class
20
+ scheduler_class: nil,
21
+ # Whether to stream the body or not
22
+ stream_body: false
18
23
  }
19
24
 
20
25
  options = DEFAULT_OPTIONS.to_a.select(&:last).to_h
21
26
 
22
27
  # Define the option parser
23
28
  OptionParser.new do |opts|
24
- opts.banner = "Usage: script.rb [options]"
29
+ opts.banner = "Usage: itsi [options]"
25
30
 
26
31
  opts.on("-w", "--workers WORKERS", Integer, "Number of workers (default: #{options[:workers]})") do |w|
27
32
  options[:workers] = w
@@ -31,29 +36,64 @@ OptionParser.new do |opts|
31
36
  options[:threads] = t
32
37
  end
33
38
 
34
- opts.on("-h", "--host HOST", String, "Host to bind to (default: #{options[:host]})") do |h|
35
- options[:host] = h
39
+ opts.on("-r", "--rackup_file FILE", String, "Rackup file to use (default: #{options[:rackup_file]})") do |rf|
40
+ options[:rackup_file] = rf
36
41
  end
37
42
 
38
- opts.on("-p", "--port PORT", Integer, "Port for the application (default: #{options[:port]})") do |p|
39
- options[:port] = p
43
+ opts.on("--worker-memory-limit MEMORY_LIMIT", Integer,
44
+ "Memory limit for each worker (default: #{options[:worker_memory_limit] || 'None'}). If this limit is breached the worker is gracefully restarted") do |ml|
45
+ options[:worker_memory_limit] = ml
40
46
  end
41
47
 
42
- opts.on("-f", "--use_fiber_scheduler PORT", TrueClass,
43
- "Port for the application (default: #{options[:use_scheduler]})") do |p|
44
- options[:use_scheduler] = p
48
+ opts.on("-f", "--fiber_scheduler [CLASS_NAME]", [String], "Scheduler class to use (default: nil). Provide blank or true to use Itsi::Scheduler, or a classname to use an alternative scheduler") do |scheduler_class|
49
+ if scheduler_class.nil? || scheduler_class == "true"
50
+ options[:scheduler_class] = "Itsi::Scheduler"
51
+ elsif scheduler_class == "false"
52
+ options.delete(:scheduler_class)
53
+ else
54
+ options[:scheduler_class] = scheduler_class
55
+ end
45
56
  end
46
57
 
47
- opts.on("--http_port HTTP_PORT", Integer, "HTTP port for the application (default: #{options[:http_port]})") do |hp|
48
- options[:http_port] = hp
58
+ opts.on("--preload [true, false, :bundle_group_name]", String, " Toggle preloading the application") do |preload|
59
+ if preload == "true"
60
+ options[:preload] = true
61
+ elsif preload == "false"
62
+ options[:preload] = false
63
+ else
64
+ # Not supported yet
65
+ end
49
66
  end
50
67
 
51
- opts.on("-c", "--cert_path CERT_PATH", String, "Path to the SSL certificate file") do |cp|
52
- options[:cert_path] = cp
68
+
69
+ opts.on("-b", "--bind BIND", String, "Bind address (default: #{options[:binds].join(", ")}). You can specify this flag multiple times to bind to multiple addresses.") do |bind|
70
+ options[:binds].pop if options[:binds].first.frozen?
71
+ options[:binds] << bind
53
72
  end
54
73
 
55
- opts.on("-k", "--key_path KEY_PATH", String, "Path to the SSL key file") do |kp|
56
- options[:key_path] = kp
74
+ opts.on("-c", "--cert_path CERT_PATH", String,
75
+ "Path to the SSL certificate file (must follow a --bind option). You can specify this flag multiple times.") do |cp|
76
+ raise OptionParser::InvalidOption, "--cert_path must follow a --bind" if options[:binds].empty?
77
+ require "uri"
78
+
79
+ # Modify the last bind entry to add/update the cert query parameter
80
+ uri = URI.parse("http://#{options[:binds].last}") # Ensure valid URI parsing
81
+ params = URI.decode_www_form(uri.query.to_s).to_h
82
+ params["cert"] = cp
83
+ query_string = params.map { |k, v| "#{k}=#{v}" }.join("&")
84
+ options[:binds][-1] = "#{uri.host}?#{query_string}"
85
+ end
86
+
87
+ opts.on("-k", "--key_path KEY_PATH", String, "Path to the SSL key file (must follow a --bind option). You can specify this flag multiple times.") do |kp|
88
+ raise OptionParser::InvalidOption, "--key_path must follow a --bind" if options[:binds].empty?
89
+ require "uri"
90
+
91
+ # Modify the last bind entry to add/update the key query parameter
92
+ uri = URI.parse("http://#{options[:binds].last}") # Ensure valid URI parsing
93
+ params = URI.decode_www_form(uri.query.to_s).to_h
94
+ params["key"] = kp
95
+ query_string = params.map { |k, v| "#{k}=#{v}" }.join("&")
96
+ options[:binds][-1] = "#{uri.host}?#{query_string}"
57
97
  end
58
98
 
59
99
  opts.on("--shutdown_timeout SHUTDOWN_TIMEOUT", String,
@@ -65,20 +105,40 @@ OptionParser.new do |opts|
65
105
  options[:script_name] = script_name
66
106
  end
67
107
 
68
- opts.on("--help", "Show this help message") do
108
+ opts.on("--stream-body", TrueClass, "Stream body frames (default: false for best compatibility)") do |stream_body|
109
+ options[:stream_body] = stream_body
110
+ end
111
+
112
+ opts.on("-h", "--help", "Show this help message") do
69
113
  puts opts
70
114
  exit
71
115
  end
72
116
  end.parse!
73
117
 
74
- # Parse the Rack application
75
- app, _ = Rack::Builder.parse_file("config.ru")
118
+ # Rack app loader, invoked per worker.
119
+ # This is a no-op if preloading is enabled (we just return the preloaded app).
120
+ preloader = \
121
+ if options[:preload]
122
+ require "rack"
123
+
124
+ app = Array(Rack::Builder.parse_file(options[:rackup_file])).first
125
+ require_relative "../lib/itsi/server/scheduler_mode" if options[:scheduler_class]
126
+ app.method(:itself).to_proc
127
+ else
128
+ lambda do
129
+ require "rack"
130
+
131
+ app = Array(Rack::Builder.parse_file(options[:rackup_file])).first
132
+ require_relative "../lib/itsi/server/scheduler_mode" if options[:scheduler_class]
133
+ app
134
+ end
135
+ end
76
136
 
77
- puts "App is #{app}"
78
- # Make sure osprey is loaded, if not already loaded by the rack_app above.
79
- # Start the Osprey server
137
+ # Make sure Itsi is loaded, if not already loaded by the rack_app above.
138
+ # Start the Itsi server
80
139
  require "itsi/server"
140
+
81
141
  Itsi::Server.new(
82
- app: app,
83
- **options
142
+ app: preloader,
143
+ **options.except(:preload, :rackup_file)
84
144
  ).start
@@ -7,3 +7,5 @@ edition = "2024"
7
7
  thiserror = "2.0.11"
8
8
  magnus = { version = "0.7.1" }
9
9
  rcgen = "0.13.2"
10
+ nix = "0.29.0"
11
+ httparse = "1.10.1"
@@ -0,0 +1,70 @@
1
+ use crate::ItsiError;
2
+ use std::ffi::NulError;
3
+
4
+ pub static CLIENT_CONNECTION_CLOSED: &str = "Client disconnected";
5
+
6
+ impl From<httparse::Error> for ItsiError {
7
+ fn from(err: httparse::Error) -> Self {
8
+ ItsiError::ArgumentError(err.to_string())
9
+ }
10
+ }
11
+
12
+ impl From<nix::errno::Errno> for ItsiError {
13
+ fn from(err: nix::errno::Errno) -> Self {
14
+ ItsiError::ArgumentError(err.to_string())
15
+ }
16
+ }
17
+
18
+ impl From<std::io::Error> for ItsiError {
19
+ fn from(err: std::io::Error) -> Self {
20
+ ItsiError::ArgumentError(err.to_string())
21
+ }
22
+ }
23
+
24
+ impl From<rcgen::Error> for ItsiError {
25
+ fn from(err: rcgen::Error) -> Self {
26
+ ItsiError::ArgumentError(err.to_string())
27
+ }
28
+ }
29
+
30
+ impl From<NulError> for ItsiError {
31
+ fn from(err: NulError) -> Self {
32
+ ItsiError::ArgumentError(err.to_string())
33
+ }
34
+ }
35
+
36
+ impl From<magnus::Error> for ItsiError {
37
+ fn from(err: magnus::Error) -> Self {
38
+ match err.error_type() {
39
+ magnus::error::ErrorType::Jump(tag) => ItsiError::Jump(tag.to_string()),
40
+ magnus::error::ErrorType::Error(_exception_class, cow) => {
41
+ ItsiError::ArgumentError(cow.to_string())
42
+ }
43
+ magnus::error::ErrorType::Exception(exception) => {
44
+ ItsiError::ArgumentError(exception.to_string())
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ impl From<ItsiError> for magnus::Error {
51
+ fn from(err: ItsiError) -> Self {
52
+ match err {
53
+ ItsiError::InvalidInput(msg) => magnus::Error::new(magnus::exception::arg_error(), msg),
54
+ ItsiError::InternalServerError(msg) => {
55
+ magnus::Error::new(magnus::exception::exception(), msg)
56
+ }
57
+ ItsiError::UnsupportedProtocol(msg) => {
58
+ magnus::Error::new(magnus::exception::arg_error(), msg)
59
+ }
60
+ ItsiError::ArgumentError(msg) => {
61
+ magnus::Error::new(magnus::exception::arg_error(), msg)
62
+ }
63
+ ItsiError::Jump(msg) => magnus::Error::new(magnus::exception::local_jump_error(), msg),
64
+ ItsiError::Break() => magnus::Error::new(magnus::exception::interrupt(), "Break"),
65
+ ItsiError::ClientConnectionClosed => {
66
+ magnus::Error::new(magnus::exception::eof_error(), CLIENT_CONNECTION_CLOSED)
67
+ }
68
+ }
69
+ }
70
+ }
@@ -1,49 +1,22 @@
1
+ pub mod from;
1
2
  use thiserror::Error;
2
3
 
3
4
  pub type Result<T> = std::result::Result<T, ItsiError>;
4
5
 
5
6
  #[derive(Error, Debug)]
6
7
  pub enum ItsiError {
7
- #[error("Invalid input")]
8
+ #[error("Invalid input {0}")]
8
9
  InvalidInput(String),
9
- #[error("Internal server error")]
10
- InternalServerError,
11
- #[error("Unsupported protocol")]
10
+ #[error("Internal server error {0}")]
11
+ InternalServerError(String),
12
+ #[error("Unsupported protocol {0}")]
12
13
  UnsupportedProtocol(String),
13
- #[error("Argument error")]
14
+ #[error("Argument error: {0}")]
14
15
  ArgumentError(String),
16
+ #[error("Client Connection Closed")]
17
+ ClientConnectionClosed,
15
18
  #[error("Jump")]
16
19
  Jump(String),
17
- }
18
-
19
- impl From<ItsiError> for magnus::Error {
20
- fn from(err: ItsiError) -> Self {
21
- magnus::Error::new(magnus::exception::runtime_error(), err.to_string())
22
- }
23
- }
24
-
25
- impl From<std::io::Error> for ItsiError {
26
- fn from(err: std::io::Error) -> Self {
27
- ItsiError::ArgumentError(err.to_string())
28
- }
29
- }
30
-
31
- impl From<rcgen::Error> for ItsiError {
32
- fn from(err: rcgen::Error) -> Self {
33
- ItsiError::ArgumentError(err.to_string())
34
- }
35
- }
36
-
37
- impl From<magnus::Error> for ItsiError {
38
- fn from(err: magnus::Error) -> Self {
39
- match err.error_type() {
40
- magnus::error::ErrorType::Jump(tag) => ItsiError::Jump(tag.to_string()),
41
- magnus::error::ErrorType::Error(_exception_class, cow) => {
42
- ItsiError::ArgumentError(cow.to_string())
43
- }
44
- magnus::error::ErrorType::Exception(exception) => {
45
- ItsiError::ArgumentError(exception.to_string())
46
- }
47
- }
48
- }
20
+ #[error("Break")]
21
+ Break(),
49
22
  }
@@ -0,0 +1,15 @@
1
+ [package]
2
+ name = "itsi_instrument_entry"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
+ license = "MIT"
7
+ publish = false
8
+
9
+
10
+ [lib]
11
+ proc-macro = true
12
+ [dependencies]
13
+ proc-macro2 = "1.0"
14
+ quote = "1.0"
15
+ syn = { version = "1.0", features = ["full"] }
@@ -0,0 +1,31 @@
1
+ use proc_macro::TokenStream;
2
+ use proc_macro2::TokenStream as TokenStream2;
3
+ use quote::quote;
4
+ use syn::{parse_macro_input, ItemFn};
5
+
6
+ #[proc_macro_attribute]
7
+ pub fn instrument_with_entry(attr: TokenStream, item: TokenStream) -> TokenStream {
8
+ let attr_tokens = TokenStream2::from(attr);
9
+ let input_fn = parse_macro_input!(item as ItemFn);
10
+ let attrs = input_fn.attrs;
11
+ let vis = input_fn.vis;
12
+ let sig = input_fn.sig;
13
+ let block = input_fn.block;
14
+ let output = quote! {
15
+ #[cfg(debug_assertions)]
16
+ #[tracing::instrument(#attr_tokens)]
17
+ #(#attrs)*
18
+ #vis #sig {
19
+ tracing::trace!("");
20
+ #block
21
+ }
22
+
23
+ #[cfg(not(debug_assertions))]
24
+ #(#attrs)*
25
+ #vis #sig {
26
+ #block
27
+ }
28
+ };
29
+
30
+ output.into()
31
+ }
@@ -4,5 +4,7 @@ version = "0.1.0"
4
4
  edition = "2024"
5
5
 
6
6
  [dependencies]
7
+ cfg-if = "1.0.0"
7
8
  magnus = { version = "0.7.1", features = ["rb-sys", "bytes"] }
9
+ nix = "0.29.0"
8
10
  rb-sys = "0.9.105"
@@ -0,0 +1,121 @@
1
+ use magnus::IntoValue;
2
+ use magnus::rb_sys::AsRawValue;
3
+ use magnus::value::BoxValue;
4
+ use magnus::{Ruby, Value, value::ReprValue};
5
+ use std::fmt::{self, Debug, Formatter};
6
+ use std::ops::Deref;
7
+
8
+ /// HeapVal is a wrapper for heap-allocated magnus ReprVa;ies
9
+ /// that is marked as thread-safe(Send and Sync)
10
+ /// It's up to the user to actually ensure this though,
11
+ /// typically by only interacting with the value from a thread which
12
+ /// holds the GVL.
13
+ pub struct HeapValue<T>(pub BoxValue<T>)
14
+ where
15
+ T: ReprValue;
16
+
17
+ impl<T> PartialEq for HeapValue<T>
18
+ where
19
+ T: ReprValue,
20
+ {
21
+ fn eq(&self, other: &Self) -> bool {
22
+ self.0.as_raw() == other.0.as_raw()
23
+ }
24
+ }
25
+
26
+ impl<T> Deref for HeapValue<T>
27
+ where
28
+ T: ReprValue,
29
+ {
30
+ type Target = T;
31
+
32
+ fn deref(&self) -> &Self::Target {
33
+ &self.0
34
+ }
35
+ }
36
+
37
+ impl<T> HeapValue<T>
38
+ where
39
+ T: ReprValue,
40
+ {
41
+ pub fn inner(self) -> T {
42
+ *self.0
43
+ }
44
+ }
45
+
46
+ impl<T> IntoValue for HeapValue<T>
47
+ where
48
+ T: ReprValue,
49
+ {
50
+ fn into_value_with(self, _: &Ruby) -> Value {
51
+ self.0.into_value()
52
+ }
53
+ }
54
+
55
+ impl<T> From<T> for HeapValue<T>
56
+ where
57
+ T: ReprValue,
58
+ {
59
+ fn from(value: T) -> Self {
60
+ HeapValue(BoxValue::new(value))
61
+ }
62
+ }
63
+
64
+ impl<T> Clone for HeapValue<T>
65
+ where
66
+ T: ReprValue + Clone,
67
+ {
68
+ fn clone(&self) -> Self {
69
+ HeapValue(BoxValue::new(*self.0.deref()))
70
+ }
71
+ }
72
+
73
+ impl<T> Debug for HeapValue<T>
74
+ where
75
+ T: ReprValue + Debug,
76
+ {
77
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
78
+ write!(f, "{:?}", self.0)
79
+ }
80
+ }
81
+
82
+ unsafe impl<T> Send for HeapValue<T> where T: ReprValue {}
83
+ unsafe impl<T> Sync for HeapValue<T> where T: ReprValue {}
84
+
85
+ /// HeapVal is a wrapper for heap-allocated magnus Values
86
+ /// that is marked as thread-safe(Send and Sync)
87
+ /// It's up to the user to actually ensure this though,
88
+ /// typically by only interacting with the value from a thread which
89
+ /// holds the GVL.
90
+ pub struct HeapVal(HeapValue<Value>);
91
+ impl Deref for HeapVal {
92
+ type Target = Value;
93
+
94
+ fn deref(&self) -> &Self::Target {
95
+ &self.0
96
+ }
97
+ }
98
+
99
+ impl IntoValue for HeapVal {
100
+ fn into_value_with(self, _: &Ruby) -> Value {
101
+ self.0.into_value()
102
+ }
103
+ }
104
+
105
+ impl From<Value> for HeapVal {
106
+ fn from(value: Value) -> Self {
107
+ HeapVal(HeapValue(BoxValue::new(value)))
108
+ }
109
+ }
110
+
111
+ impl Clone for HeapVal {
112
+ fn clone(&self) -> Self {
113
+ HeapVal(HeapValue(BoxValue::new(*self.0.deref())))
114
+ }
115
+ }
116
+
117
+ impl Debug for HeapVal {
118
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
119
+ write!(f, "{:?}", self.0)
120
+ }
121
+ }
@@ -1,23 +1,43 @@
1
- use std::{os::raw::c_void, ptr::null_mut};
1
+ use std::{os::raw::c_void, ptr::null_mut, sync::Arc};
2
2
 
3
+ use magnus::{
4
+ RArray, Ruby, Thread, Value,
5
+ rb_sys::FromRawValue,
6
+ value::{LazyId, ReprValue},
7
+ };
3
8
  use rb_sys::{
4
- rb_thread_call_with_gvl, rb_thread_call_without_gvl, rb_thread_create, rb_thread_wakeup,
9
+ rb_thread_call_with_gvl, rb_thread_call_without_gvl, rb_thread_create, rb_thread_schedule,
10
+ rb_thread_wakeup,
5
11
  };
6
12
 
7
- pub fn create_ruby_thread<F>(f: F)
13
+ mod heap_value;
14
+ pub use heap_value::{HeapVal, HeapValue};
15
+ static ID_FORK: LazyId = LazyId::new("fork");
16
+ static ID_LIST: LazyId = LazyId::new("list");
17
+ static ID_EQ: LazyId = LazyId::new("==");
18
+ static ID_ALIVE: LazyId = LazyId::new("alive?");
19
+ static ID_THREAD_VARIABLE_GET: LazyId = LazyId::new("thread_variable_get");
20
+
21
+ pub fn schedule_thread() {
22
+ unsafe {
23
+ rb_thread_schedule();
24
+ };
25
+ }
26
+ pub fn create_ruby_thread<F>(f: F) -> Thread
8
27
  where
9
- F: FnOnce() -> u64 + Send + 'static,
28
+ F: FnOnce() + Send + 'static,
10
29
  {
11
30
  extern "C" fn trampoline<F>(ptr: *mut c_void) -> u64
12
31
  where
13
- F: FnOnce() -> u64,
32
+ F: FnOnce(),
14
33
  {
15
34
  // Reconstruct the boxed Option<F> that holds our closure.
16
35
  let boxed_closure: Box<Option<F>> = unsafe { Box::from_raw(ptr as *mut Option<F>) };
17
36
  // Extract the closure. (The Option should be Some; panic otherwise.)
18
37
  let closure = (*boxed_closure).expect("Closure already taken");
19
38
  // Call the closure and return its result.
20
- closure()
39
+ closure();
40
+ 0
21
41
  }
22
42
 
23
43
  // Box the closure (wrapped in an Option) to create a stable pointer.
@@ -26,7 +46,10 @@ where
26
46
 
27
47
  // Call rb_thread_create with our trampoline and boxed closure.
28
48
  unsafe {
29
- rb_thread_wakeup(rb_thread_create(Some(trampoline::<F>), ptr));
49
+ let thread = rb_thread_create(Some(trampoline::<F>), ptr);
50
+ rb_thread_wakeup(thread);
51
+ rb_thread_schedule();
52
+ Thread::from_value(Value::from_raw(thread)).unwrap()
30
53
  }
31
54
  }
32
55
 
@@ -67,18 +90,18 @@ where
67
90
 
68
91
  pub fn call_with_gvl<F, R>(f: F) -> R
69
92
  where
70
- F: FnOnce() -> R,
93
+ F: FnOnce(Ruby) -> R,
71
94
  {
72
95
  extern "C" fn trampoline<F, R>(arg: *mut c_void) -> *mut c_void
73
96
  where
74
- F: FnOnce() -> R,
97
+ F: FnOnce(Ruby) -> R,
75
98
  {
76
99
  // 1) Reconstruct the Box that holds our closure
77
100
  let closure_ptr = arg as *mut Option<F>;
78
101
  let closure = unsafe { (*closure_ptr).take().expect("Closure already taken") };
79
102
 
80
103
  // 2) Call the user’s closure
81
- let result = closure();
104
+ let result = closure(Ruby::get().unwrap());
82
105
 
83
106
  // 3) Box up the result so we can return a pointer to it
84
107
  let boxed_result = Box::new(result);
@@ -96,3 +119,60 @@ where
96
119
  let result_box = unsafe { Box::from_raw(raw_result_ptr as *mut R) };
97
120
  *result_box
98
121
  }
122
+
123
+ pub fn fork(after_fork: Arc<Option<impl Fn()>>) -> Option<i32> {
124
+ let ruby = Ruby::get().unwrap();
125
+ let fork_result = ruby
126
+ .module_kernel()
127
+ .funcall::<_, _, Option<i32>>(*ID_FORK, ())
128
+ .unwrap();
129
+ if fork_result.is_none() {
130
+ if let Some(f) = &*after_fork {
131
+ f()
132
+ }
133
+ }
134
+ fork_result
135
+ }
136
+
137
+ pub fn kill_threads<T>(threads: Vec<T>)
138
+ where
139
+ T: ReprValue,
140
+ {
141
+ for thr in &threads {
142
+ let alive: bool = thr
143
+ .funcall(*ID_ALIVE, ())
144
+ .expect("Failed to check if thread is alive");
145
+ if !alive {
146
+ eprintln!("Thread killed");
147
+ break;
148
+ }
149
+ eprintln!("Killing thread {:?}", thr.as_value());
150
+ thr.funcall::<_, _, Value>("terminate", ())
151
+ .expect("Failed to kill thread");
152
+ }
153
+ }
154
+
155
+ pub fn terminate_non_fork_safe_threads() {
156
+ let ruby = Ruby::get().unwrap();
157
+ let thread_class = ruby.class_thread();
158
+ let current: Thread = ruby.thread_current();
159
+ let threads: RArray = thread_class
160
+ .funcall(*ID_LIST, ())
161
+ .expect("Failed to list Ruby threads");
162
+
163
+ let non_fork_safe_threads = threads
164
+ .into_iter()
165
+ .filter_map(|v| {
166
+ let v_thread = Thread::from_value(v).unwrap();
167
+ let non_fork_safe = !v_thread
168
+ .funcall::<_, _, bool>(*ID_EQ, (current,))
169
+ .unwrap_or(false)
170
+ && !v_thread
171
+ .funcall::<_, _, bool>(*ID_THREAD_VARIABLE_GET, (ruby.sym_new("fork_safe"),))
172
+ .unwrap_or(false);
173
+ if non_fork_safe { Some(v_thread) } else { None }
174
+ })
175
+ .collect::<Vec<_>>();
176
+
177
+ kill_threads(non_fork_safe_threads);
178
+ }
@@ -0,0 +1,24 @@
1
+ [package]
2
+ name = "itsi-scheduler"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
+ license = "MIT"
7
+ publish = false
8
+
9
+ [lib]
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ magnus = { version = "0.7.1", features = ["rb-sys", "bytes"] }
14
+ derive_more = { version = "2.0.1", features = ["debug"] }
15
+ itsi_tracing = { path = "../itsi_tracing" }
16
+ itsi_rb_helpers = { path = "../itsi_rb_helpers" }
17
+ itsi_error = { path = "../itsi_error" }
18
+ itsi_instrument_entry = { path = "../itsi_instrument_entry" }
19
+ parking_lot = "0.12.3"
20
+ mio = { version = "1.0.3", features = ["os-poll", "os-ext"] }
21
+ rb-sys = "0.9.105"
22
+ bytes = "1.10.1"
23
+ nix = "0.29.0"
24
+ tracing = "0.1.41"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("scheduler/itsi_scheduler")