itsi 0.1.9 → 0.1.11

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +11 -2
  3. data/Rakefile +5 -2
  4. data/crates/itsi_rb_helpers/src/lib.rs +27 -4
  5. data/crates/itsi_server/Cargo.toml +4 -1
  6. data/crates/itsi_server/src/lib.rs +69 -1
  7. data/crates/itsi_server/src/request/itsi_request.rs +2 -9
  8. data/crates/itsi_server/src/response/itsi_response.rs +2 -2
  9. data/crates/itsi_server/src/server/bind.rs +16 -12
  10. data/crates/itsi_server/src/server/itsi_server.rs +43 -49
  11. data/crates/itsi_server/src/server/listener.rs +9 -9
  12. data/crates/itsi_server/src/server/process_worker.rs +10 -3
  13. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
  14. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +124 -111
  15. data/crates/itsi_server/src/server/signal.rs +1 -4
  16. data/crates/itsi_server/src/server/thread_worker.rs +52 -20
  17. data/crates/itsi_server/src/server/tls.rs +1 -1
  18. data/gems/scheduler/ext/itsi_rb_helpers/src/lib.rs +27 -4
  19. data/gems/scheduler/ext/itsi_server/Cargo.toml +4 -1
  20. data/gems/scheduler/ext/itsi_server/src/lib.rs +69 -1
  21. data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +2 -9
  22. data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +2 -2
  23. data/gems/scheduler/ext/itsi_server/src/server/bind.rs +16 -12
  24. data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +43 -49
  25. data/gems/scheduler/ext/itsi_server/src/server/listener.rs +9 -9
  26. data/gems/scheduler/ext/itsi_server/src/server/process_worker.rs +10 -3
  27. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
  28. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +124 -111
  29. data/gems/scheduler/ext/itsi_server/src/server/signal.rs +1 -4
  30. data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +52 -20
  31. data/gems/scheduler/ext/itsi_server/src/server/tls.rs +1 -1
  32. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  33. data/gems/server/Cargo.lock +11 -2
  34. data/gems/server/exe/itsi +53 -23
  35. data/gems/server/ext/itsi_rb_helpers/src/lib.rs +27 -4
  36. data/gems/server/ext/itsi_server/Cargo.toml +4 -1
  37. data/gems/server/ext/itsi_server/src/lib.rs +69 -1
  38. data/gems/server/ext/itsi_server/src/request/itsi_request.rs +2 -9
  39. data/gems/server/ext/itsi_server/src/response/itsi_response.rs +2 -2
  40. data/gems/server/ext/itsi_server/src/server/bind.rs +16 -12
  41. data/gems/server/ext/itsi_server/src/server/itsi_server.rs +43 -49
  42. data/gems/server/ext/itsi_server/src/server/listener.rs +9 -9
  43. data/gems/server/ext/itsi_server/src/server/process_worker.rs +10 -3
  44. data/gems/server/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
  45. data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +124 -111
  46. data/gems/server/ext/itsi_server/src/server/signal.rs +1 -4
  47. data/gems/server/ext/itsi_server/src/server/thread_worker.rs +52 -20
  48. data/gems/server/ext/itsi_server/src/server/tls.rs +1 -1
  49. data/gems/server/lib/itsi/server/Itsi.rb +127 -0
  50. data/gems/server/lib/itsi/server/config.rb +36 -0
  51. data/gems/server/lib/itsi/server/options_dsl.rb +401 -0
  52. data/gems/server/lib/itsi/server/rack/handler/itsi.rb +18 -6
  53. data/gems/server/lib/itsi/server/rack_interface.rb +1 -5
  54. data/gems/server/lib/itsi/server/signal_trap.rb +0 -1
  55. data/gems/server/lib/itsi/server/version.rb +1 -1
  56. data/gems/server/lib/itsi/server.rb +7 -3
  57. data/gems/server/test/helpers/test_helper.rb +7 -5
  58. data/gems/server/test/test_itsi_server.rb +21 -2
  59. data/lib/itsi/version.rb +1 -1
  60. data/location_dsl.rb +381 -0
  61. data/sandbox/itsi_itsi_file/Itsi.rb +119 -0
  62. data/sandbox/itsi_sandbox_async/Gemfile +1 -1
  63. data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
  64. data/sandbox/itsi_sandbox_rails/Gemfile.lock +2 -2
  65. data/tasks.txt +27 -4
  66. metadata +14 -9
@@ -1,7 +1,8 @@
1
- use std::{os::raw::c_void, ptr::null_mut, sync::Arc};
1
+ use std::{os::raw::c_void, ptr::null_mut};
2
2
 
3
3
  use magnus::{
4
4
  RArray, Ruby, Thread, Value,
5
+ block::Proc,
5
6
  rb_sys::FromRawValue,
6
7
  value::{LazyId, ReprValue},
7
8
  };
@@ -17,6 +18,7 @@ static ID_LIST: LazyId = LazyId::new("list");
17
18
  static ID_EQ: LazyId = LazyId::new("==");
18
19
  static ID_ALIVE: LazyId = LazyId::new("alive?");
19
20
  static ID_THREAD_VARIABLE_GET: LazyId = LazyId::new("thread_variable_get");
21
+ static ID_BACKTRACE: LazyId = LazyId::new("backtrace");
20
22
 
21
23
  pub fn schedule_thread() {
22
24
  unsafe {
@@ -120,20 +122,30 @@ where
120
122
  *result_box
121
123
  }
122
124
 
123
- pub fn fork(after_fork: Arc<Option<impl Fn()>>) -> Option<i32> {
125
+ pub fn fork(after_fork: Option<HeapValue<Proc>>) -> Option<i32> {
124
126
  let ruby = Ruby::get().unwrap();
125
127
  let fork_result = ruby
126
128
  .module_kernel()
127
129
  .funcall::<_, _, Option<i32>>(*ID_FORK, ())
128
130
  .unwrap();
129
131
  if fork_result.is_none() {
130
- if let Some(f) = &*after_fork {
131
- f()
132
+ if let Some(proc) = after_fork {
133
+ call_proc_and_log_errors(proc)
132
134
  }
133
135
  }
134
136
  fork_result
135
137
  }
136
138
 
139
+ pub fn call_proc_and_log_errors(proc: HeapValue<Proc>) {
140
+ if let Err(e) = proc.call::<_, Value>(()) {
141
+ if let Some(value) = e.value() {
142
+ print_rb_backtrace(value);
143
+ } else {
144
+ eprintln!("Error occurred {:?}", e);
145
+ }
146
+ }
147
+ }
148
+
137
149
  pub fn kill_threads<T>(threads: Vec<T>)
138
150
  where
139
151
  T: ReprValue,
@@ -176,3 +188,14 @@ pub fn terminate_non_fork_safe_threads() {
176
188
 
177
189
  kill_threads(non_fork_safe_threads);
178
190
  }
191
+
192
+ pub fn print_rb_backtrace(rb_err: Value) {
193
+ let backtrace = rb_err
194
+ .funcall::<_, _, Vec<String>>(*ID_BACKTRACE, ())
195
+ .unwrap_or_default();
196
+
197
+ eprintln!("Ruby exception {:?}", rb_err);
198
+ for line in backtrace {
199
+ eprintln!("{}", line);
200
+ }
201
+ }
@@ -24,7 +24,7 @@ rcgen = { version = "0.13.2", features = ["x509-parser", "pem"] }
24
24
  base64 = "0.22.1"
25
25
  http-body-util = "0.1.2"
26
26
  hyper = { version = "1.5.0", features = ["full", "server", "http1", "http2"] }
27
- tokio = { version = "1", features = ["full"] }
27
+ tokio = { version = "1.44.1", features = ["full"] }
28
28
  hyper-util = { version = "0.1.10", features = ["full"] }
29
29
  derive_more = { version = "2.0.1", features = ["debug"] }
30
30
  http = "1.3.1"
@@ -45,3 +45,6 @@ fs2 = "0.4.3"
45
45
  ring = "0.17.14"
46
46
  async-trait = "0.1.87"
47
47
  dirs = "6.0.0"
48
+ regex = "1.11.1"
49
+ route-recognizer = "0.3.1"
50
+ fnv = "1.0.7"
@@ -1,5 +1,8 @@
1
1
  use body_proxy::itsi_body_proxy::ItsiBodyProxy;
2
- use magnus::{error::Result, function, method, value::Lazy, Module, Object, RClass, RModule, Ruby};
2
+ use magnus::{
3
+ error::Result, function, method, value::Lazy, Module, Object, RClass, RHash, RModule, Ruby,
4
+ };
5
+ use regex::{Regex, RegexSet};
3
6
  use request::itsi_request::ItsiRequest;
4
7
  use response::itsi_response::ItsiResponse;
5
8
  use server::{itsi_server::Server, signal::reset_signal_handlers};
@@ -54,6 +57,70 @@ pub fn log_error(msg: String) {
54
57
  error!(msg);
55
58
  }
56
59
 
60
+ const ROUTES: [&str; 39] = [
61
+ r"(?-u)^/organisations/(?<organisation_id>\d+)/users/(?<user_id>\d+)$",
62
+ r"(?-u)^/projects/(?<project_id>\d+)/tasks/(?<task_id>\d+)$",
63
+ r"(?-u)^/products/(?<product_id>\d+)(?:/reviews/(?<review_id>\d+))?$",
64
+ r"(?-u)^/orders/(?<order_id>\d+)/items(?:/(?<item_id>\d+))?$",
65
+ r"(?-u)^/posts/(?<post_id>\d+)/comments(?:/(?<comment_id>\d+))?$",
66
+ r"(?-u)^/teams/(?<team_id>\d+)(?:/members/(?<member_id>\d+))?$",
67
+ r"(?-u)^/categories/(?<category_id>\d+)/subcategories(?:/(?<subcategory_id>\d+))?$",
68
+ r"(?-u)^/departments/(?<department_id>\d+)/employees/(?<employee_id>\d+)$",
69
+ r"(?-u)^/events/(?<event_id>\d+)(?:/sessions/(?<session_id>\d+))?$",
70
+ r"(?-u)^/invoices/(?<invoice_id>\d+)/payments(?:/(?<payment_id>\d+))?$",
71
+ r"(?-u)^/tickets/(?<ticket_id>\d+)(?:/responses/(?<response_id>\d+))?$",
72
+ r"(?-u)^/forums/(?<forum_id>\d+)(?:/threads/(?<thread_id>\d+))?$",
73
+ r"(?-u)^/subscriptions/(?<subscription_id>\d+)/plans(?:/(?<plan_id>\d+))?$",
74
+ r"(?-u)^/profiles/(?<profile_id>\d+)/settings$",
75
+ r"(?-u)^/organizations/(?<organization_id>\d+)/billing(?:/(?<billing_id>\d+))?$",
76
+ r"(?-u)^/vendors/(?<vendor_id>\d+)/products(?:/(?<product_id>\d+))?$",
77
+ r"(?-u)^/courses/(?<course_id>\d+)/modules(?:/(?<module_id>\d+))?$",
78
+ r"(?-u)^/accounts/(?<account_id>\d+)(?:/transactions/(?<transaction_id>\d+))?$",
79
+ r"(?-u)^/warehouses/(?<warehouse_id>\d+)/inventory(?:/(?<inventory_id>\d+))?$",
80
+ r"(?-u)^/campaigns/(?<campaign_id>\d+)/ads(?:/(?<ad_id>\d+))?$",
81
+ r"(?-u)^/applications/(?<application_id>\d+)/stages(?:/(?<stage_id>\d+))?$",
82
+ r"(?-u)^/notifications/(?<notification_id>\d+)$",
83
+ r"(?-u)^/albums/(?<album_id>\d+)/photos(?:/(?<photo_id>\d+))?$",
84
+ r"(?-u)^/news/(?<news_id>\d+)/articles(?:/(?<article_id>\d+))?$",
85
+ r"(?-u)^/libraries/(?<library_id>\d+)/books(?:/(?<book_id>\d+))?$",
86
+ r"(?-u)^/universities/(?<university_id>\d+)/students(?:/(?<student_id>\d+))?$",
87
+ r"(?-u)^/banks/(?<bank_id>\d+)/branches(?:/(?<branch_id>\d+))?$",
88
+ r"(?-u)^/vehicles/(?<vehicle_id>\d+)/services(?:/(?<service_id>\d+))?$",
89
+ r"(?-u)^/hotels/(?<hotel_id>\d+)/rooms(?:/(?<room_id>\d+))?$",
90
+ r"(?-u)^/doctors/(?<doctor_id>\d+)/appointments(?:/(?<appointment_id>\d+))?$",
91
+ r"(?-u)^/gyms/(?<gym_id>\d+)/memberships(?:/(?<membership_id>\d+))?$",
92
+ r"(?-u)^/restaurants/(?<restaurant_id>\d+)/menus(?:/(?<menu_id>\d+))?$",
93
+ r"(?-u)^/parks/(?<park_id>\d+)/events(?:/(?<event_id>\d+))?$",
94
+ r"(?-u)^/theaters/(?<theater_id>\d+)/shows(?:/(?<show_id>\d+))?$",
95
+ r"(?-u)^/museums/(?<museum_id>\d+)/exhibits(?:/(?<exhibit_id>\d+))?$",
96
+ r"(?-u)^/stadiums/(?<stadium_id>\d+)/games(?:/(?<game_id>\d+))?$",
97
+ r"(?-u)^/schools/(?<school_id>\d+)/classes(?:/(?<class_id>\d+))?$",
98
+ r"(?-u)^/clubs/(?<club_id>\d+)/events(?:/(?<event_id>\d+))?$",
99
+ r"(?-u)^/festivals/(?<festival_id>\d+)/tickets(?:/(?<ticket_id>\d+))?$",
100
+ ];
101
+ use std::sync::LazyLock;
102
+
103
+ static REGEX_SET: LazyLock<RegexSet> = LazyLock::new(|| RegexSet::new(ROUTES).unwrap());
104
+ static REGEXES: LazyLock<Vec<Regex>> =
105
+ LazyLock::new(|| ROUTES.iter().map(|&r| Regex::new(r).unwrap()).collect());
106
+
107
+ fn match_route(input: String) -> Result<Option<(usize, Option<RHash>)>> {
108
+ if let Some(index) = REGEX_SET.matches(&input).iter().next() {
109
+ let regex = &REGEXES[index];
110
+ if let Some(captures) = regex.captures(&input) {
111
+ let params = RHash::with_capacity(captures.len());
112
+ for name in regex.capture_names().flatten() {
113
+ if let Some(value) = captures.name(name) {
114
+ params.aset(name, value.as_str()).ok();
115
+ }
116
+ }
117
+ return Ok(Some((index, Some(params))));
118
+ }
119
+ return Ok(Some((index, None)));
120
+ }
121
+ Ok(None)
122
+ }
123
+
57
124
  #[magnus::init]
58
125
  fn init(ruby: &Ruby) -> Result<()> {
59
126
  itsi_tracing::init();
@@ -62,6 +129,7 @@ fn init(ruby: &Ruby) -> Result<()> {
62
129
  .ok();
63
130
 
64
131
  let itsi = ruby.get_inner(&ITSI_MODULE);
132
+ itsi.define_singleton_method("match_route", function!(match_route, 1))?;
65
133
  itsi.define_singleton_method("log_debug", function!(log_debug, 1))?;
66
134
  itsi.define_singleton_method("log_info", function!(log_info, 1))?;
67
135
  itsi.define_singleton_method("log_warn", function!(log_warn, 1))?;
@@ -17,6 +17,7 @@ use http::{request::Parts, HeaderValue, Response, StatusCode};
17
17
  use http_body_util::{combinators::BoxBody, BodyExt, Empty};
18
18
  use hyper::{body::Incoming, Request};
19
19
  use itsi_error::from::CLIENT_CONNECTION_CLOSED;
20
+ use itsi_rb_helpers::print_rb_backtrace;
20
21
  use itsi_tracing::{debug, error};
21
22
  use magnus::{
22
23
  error::{ErrorType, Result as MagnusResult},
@@ -33,7 +34,6 @@ use tokio::sync::{
33
34
  };
34
35
  static ID_CALL: LazyId = LazyId::new("call");
35
36
  static ID_MESSAGE: LazyId = LazyId::new("message");
36
- static ID_BACKTRACE: LazyId = LazyId::new("backtrace");
37
37
 
38
38
  #[derive(Debug)]
39
39
  #[magnus::wrap(class = "Itsi::Request", free_immediately, size)]
@@ -115,14 +115,7 @@ impl ItsiRequest {
115
115
  debug!("Connection closed by client");
116
116
  response.close();
117
117
  } else if let Some(rb_err) = err.value() {
118
- let backtrace = rb_err
119
- .funcall::<_, _, Vec<String>>(*ID_BACKTRACE, ())
120
- .unwrap_or_default();
121
-
122
- error!("Error occurred in Handler: {:?}", rb_err);
123
- for line in backtrace {
124
- error!("{}", line);
125
- }
118
+ print_rb_backtrace(rb_err);
126
119
  response.internal_server_error(err.to_string());
127
120
  } else {
128
121
  response.internal_server_error(err.to_string());
@@ -77,7 +77,6 @@ impl ItsiResponse {
77
77
  (ReceiverStream::new(receiver), shutdown_rx),
78
78
  |(mut receiver, mut shutdown_rx)| async move {
79
79
  if let RunningPhase::ShutdownPending = *shutdown_rx.borrow() {
80
- warn!("Disconnecting streaming client.");
81
80
  return None;
82
81
  }
83
82
  loop {
@@ -280,7 +279,8 @@ impl ItsiResponse {
280
279
  if let Some(writer) = writer.write().as_ref() {
281
280
  writer
282
281
  .blocking_send(Some(frame))
283
- .map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)?;
282
+ .map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)
283
+ .ok();
284
284
  }
285
285
  Ok(0)
286
286
  }
@@ -101,7 +101,7 @@ impl FromStr for Bind {
101
101
  "IPv6 addresses must use [ ] when specifying a port".to_owned(),
102
102
  ));
103
103
  } else {
104
- (h, None) // Treat as a hostname
104
+ (h, p.parse::<u16>().ok()) // Treat as a hostname
105
105
  }
106
106
  } else {
107
107
  (url, None)
@@ -110,18 +110,22 @@ impl FromStr for Bind {
110
110
  let address = if let Ok(ip) = host.parse::<IpAddr>() {
111
111
  BindAddress::Ip(ip)
112
112
  } else {
113
- resolve_hostname(host)
114
- .map(BindAddress::Ip)
115
- .ok_or(ItsiError::ArgumentError(format!(
116
- "Failed to resolve hostname {}",
117
- host
118
- )))?
113
+ match protocol {
114
+ BindProtocol::Https | BindProtocol::Http => resolve_hostname(host)
115
+ .map(BindAddress::Ip)
116
+ .ok_or(ItsiError::ArgumentError(format!(
117
+ "Failed to resolve hostname {}",
118
+ host
119
+ )))?,
120
+ BindProtocol::Unix | BindProtocol::Unixs => BindAddress::UnixSocket(host.into()),
121
+ }
119
122
  };
120
- let (port, address) = match protocol {
121
- BindProtocol::Http => (port.or(Some(80)), address),
122
- BindProtocol::Https => (port.or(Some(443)), address),
123
- BindProtocol::Unix => (None, BindAddress::UnixSocket(host.into())),
124
- BindProtocol::Unixs => (None, BindAddress::UnixSocket(host.into())),
123
+
124
+ let port = match protocol {
125
+ BindProtocol::Http => port.or(Some(80)),
126
+ BindProtocol::Https => port.or(Some(443)),
127
+ BindProtocol::Unix => None,
128
+ BindProtocol::Unixs => None,
125
129
  };
126
130
 
127
131
  let tls_config = match protocol {
@@ -8,17 +8,17 @@ use super::{
8
8
  };
9
9
  use crate::{request::itsi_request::ItsiRequest, server::serve_strategy::ServeStrategy};
10
10
  use derive_more::Debug;
11
- use itsi_rb_helpers::{call_without_gvl, HeapVal};
11
+ use itsi_rb_helpers::{call_without_gvl, HeapVal, HeapValue};
12
12
  use itsi_tracing::{error, run_silently};
13
13
  use magnus::{
14
14
  block::Proc,
15
15
  error::Result,
16
16
  scan_args::{get_kwargs, scan_args, Args, KwArgs, ScanArgsKw, ScanArgsOpt, ScanArgsRequired},
17
- value::{InnerValue, Opaque, ReprValue},
18
- ArgList, RHash, Ruby, Symbol, Value,
17
+ value::ReprValue,
18
+ ArgList, RArray, RHash, Ruby, Symbol, Value,
19
19
  };
20
20
  use parking_lot::{Mutex, RwLock};
21
- use std::{cmp::max, ops::Deref, sync::Arc};
21
+ use std::{cmp::max, collections::HashMap, ops::Deref, sync::Arc};
22
22
  use tracing::{info, instrument};
23
23
 
24
24
  static DEFAULT_BIND: &str = "http://localhost:3000";
@@ -36,7 +36,6 @@ impl Deref for Server {
36
36
  &self.config
37
37
  }
38
38
  }
39
- type AfterFork = Mutex<Arc<Option<Box<dyn Fn() + Send + Sync>>>>;
40
39
 
41
40
  #[derive(Debug)]
42
41
  pub struct ServerConfig {
@@ -51,15 +50,14 @@ pub struct ServerConfig {
51
50
  pub script_name: String,
52
51
  pub(crate) binds: Mutex<Vec<Bind>>,
53
52
  #[debug(skip)]
54
- pub before_fork: Mutex<Option<Box<dyn FnOnce() + Send + Sync>>>,
55
- #[debug(skip)]
56
- pub after_fork: AfterFork,
53
+ pub hooks: HashMap<String, HeapValue<Proc>>,
57
54
  pub scheduler_class: Option<String>,
58
55
  pub stream_body: Option<bool>,
59
56
  pub worker_memory_limit: Option<u64>,
60
57
  #[debug(skip)]
61
58
  pub(crate) strategy: RwLock<Option<ServeStrategy>>,
62
59
  pub silence: bool,
60
+ pub oob_gc_responses_threshold: Option<u64>,
63
61
  }
64
62
 
65
63
  #[derive(Debug)]
@@ -68,8 +66,6 @@ pub enum RequestJob {
68
66
  Shutdown,
69
67
  }
70
68
 
71
- // Define your helper function.
72
- // Here P, A, C correspond to the types for the first tuple, second tuple, and extra parameters respectively.
73
69
  fn extract_args<Req, Opt, Splat>(
74
70
  scan_args: &Args<(), (), (), (), RHash, ()>,
75
71
  primaries: &[&str],
@@ -80,20 +76,17 @@ where
80
76
  Opt: ScanArgsOpt,
81
77
  Splat: ScanArgsKw,
82
78
  {
83
- // Combine the primary and rest names into one Vec of Symbols.
84
79
  let symbols: Vec<Symbol> = primaries
85
80
  .iter()
86
81
  .chain(rest.iter())
87
82
  .map(|&name| Symbol::new(name))
88
83
  .collect();
89
84
 
90
- // Call the "slice" function with the combined symbols.
91
85
  let hash = scan_args
92
86
  .keywords
93
87
  .funcall::<_, _, RHash>("slice", symbols.into_arg_list_with(&Ruby::get().unwrap()))
94
88
  .unwrap();
95
89
 
96
- // Finally, call get_kwargs with the original name slices.
97
90
  get_kwargs(hash, primaries, rest)
98
91
  }
99
92
 
@@ -129,14 +122,14 @@ impl Server {
129
122
  type Args2 = KwArgs<
130
123
  (),
131
124
  (
132
- // Before Fork
133
- Option<Proc>,
134
- // After Fork
135
- Option<Proc>,
125
+ // Hooks
126
+ Option<RHash>,
136
127
  // Scheduler Class
137
128
  Option<String>,
138
129
  // Worker Memory Limit
139
130
  Option<u64>,
131
+ // Out-of-band GC Responses Threshold
132
+ Option<u64>,
140
133
  // Silence
141
134
  Option<bool>,
142
135
  ),
@@ -160,14 +153,33 @@ impl Server {
160
153
  &scan_args,
161
154
  &[],
162
155
  &[
163
- "before_fork",
164
- "after_fork",
156
+ "hooks",
165
157
  "scheduler_class",
166
158
  "worker_memory_limit",
159
+ "oob_gc_responses_threshold",
167
160
  "silence",
168
161
  ],
169
162
  )?;
170
163
 
164
+ let hooks = args2
165
+ .optional
166
+ .0
167
+ .map(|rhash| -> Result<HashMap<String, HeapValue<Proc>>> {
168
+ let mut hook_map: HashMap<String, HeapValue<Proc>> = HashMap::new();
169
+ for pair in rhash.enumeratorize::<_, ()>("each", ()) {
170
+ if let Some(pair_value) = RArray::from_value(pair?) {
171
+ if let (Ok(key), Ok(value)) =
172
+ (pair_value.entry::<Value>(0), pair_value.entry::<Proc>(1))
173
+ {
174
+ hook_map.insert(key.to_string(), HeapValue::from(value));
175
+ }
176
+ }
177
+ }
178
+ Ok(hook_map)
179
+ })
180
+ .transpose()?
181
+ .unwrap_or_default();
182
+
171
183
  let config = ServerConfig {
172
184
  app: HeapVal::from(args1.required.0),
173
185
  workers: max(args1.optional.0.unwrap_or(1), 1),
@@ -184,32 +196,16 @@ impl Server {
184
196
  .collect::<itsi_error::Result<Vec<Bind>>>()?,
185
197
  ),
186
198
  stream_body: args1.optional.5,
187
- before_fork: Mutex::new(args2.optional.0.map(|p| {
188
- let opaque_proc = Opaque::from(p);
189
- Box::new(move || {
190
- opaque_proc
191
- .get_inner_with(&Ruby::get().unwrap())
192
- .call::<_, Value>(())
193
- .unwrap();
194
- }) as Box<dyn FnOnce() + Send + Sync>
195
- })),
196
- after_fork: Mutex::new(Arc::new(args2.optional.1.map(|p| {
197
- let opaque_proc = Opaque::from(p);
198
- Box::new(move || {
199
- opaque_proc
200
- .get_inner_with(&Ruby::get().unwrap())
201
- .call::<_, Value>(())
202
- .unwrap();
203
- }) as Box<dyn Fn() + Send + Sync>
204
- }))),
205
- scheduler_class: args2.optional.2.clone(),
206
- worker_memory_limit: args2.optional.3,
207
- silence: args2.optional.4.is_some_and(|s| s),
199
+ hooks,
200
+ scheduler_class: args2.optional.1.clone(),
201
+ worker_memory_limit: args2.optional.2,
208
202
  strategy: RwLock::new(None),
203
+ oob_gc_responses_threshold: args2.optional.3,
204
+ silence: args2.optional.4.is_some_and(|s| s),
209
205
  };
210
206
 
211
207
  if !config.silence {
212
- if let Some(scheduler_class) = args2.optional.2 {
208
+ if let Some(scheduler_class) = args2.optional.1 {
213
209
  info!(scheduler_class, fiber_scheduler = true);
214
210
  } else {
215
211
  info!(fiber_scheduler = false);
@@ -222,7 +218,7 @@ impl Server {
222
218
  }
223
219
 
224
220
  #[instrument(name = "Bind", skip_all, fields(binds=format!("{:?}", self.config.binds.lock())))]
225
- pub(crate) fn build_listeners(&self) -> Result<Arc<Vec<Arc<Listener>>>> {
221
+ pub(crate) fn build_listeners(&self) -> Result<Vec<Listener>> {
226
222
  let listeners = self
227
223
  .config
228
224
  .binds
@@ -232,13 +228,13 @@ impl Server {
232
228
  .map(Listener::try_from)
233
229
  .collect::<std::result::Result<Vec<Listener>, _>>()?
234
230
  .into_iter()
235
- .map(Arc::new)
236
231
  .collect::<Vec<_>>();
237
232
  info!("Bound {:?} listeners", listeners.len());
238
- Ok(Arc::new(listeners))
233
+ Ok(listeners)
239
234
  }
240
235
 
241
- pub(crate) fn build_strategy(self, listeners: Arc<Vec<Arc<Listener>>>) -> Result<()> {
236
+ pub(crate) fn build_strategy(self) -> Result<()> {
237
+ let listeners = self.build_listeners()?;
242
238
  let server = Arc::new(self);
243
239
  let server_clone = server.clone();
244
240
 
@@ -276,11 +272,9 @@ impl Server {
276
272
  fn build_and_run_strategy(&self) -> Result<()> {
277
273
  reset_signal_handlers();
278
274
  let rself = self.clone();
279
- let listeners = self.build_listeners()?;
280
- let listeners_clone = listeners.clone();
281
275
  call_without_gvl(move || -> Result<()> {
282
- rself.clone().build_strategy(listeners_clone)?;
283
- if let Err(e) = rself.clone().strategy.read().as_ref().unwrap().run() {
276
+ rself.clone().build_strategy()?;
277
+ if let Err(e) = rself.strategy.read().as_ref().unwrap().run() {
284
278
  error!("Error running server: {}", e);
285
279
  rself.strategy.read().as_ref().unwrap().stop()?;
286
280
  }
@@ -243,20 +243,20 @@ impl std::fmt::Display for SockAddr {
243
243
  }
244
244
 
245
245
  impl Listener {
246
- pub fn to_tokio_listener(&self) -> TokioListener {
246
+ pub fn into_tokio_listener(self) -> TokioListener {
247
247
  match self {
248
- Listener::Tcp(listener) => TokioListener::Tcp(
249
- TokioTcpListener::from_std(TcpListener::try_clone(listener).unwrap()).unwrap(),
250
- ),
248
+ Listener::Tcp(listener) => {
249
+ TokioListener::Tcp(TokioTcpListener::from_std(listener).unwrap())
250
+ }
251
251
  Listener::TcpTls((listener, acceptor)) => TokioListener::TcpTls(
252
- TokioTcpListener::from_std(TcpListener::try_clone(listener).unwrap()).unwrap(),
252
+ TokioTcpListener::from_std(listener).unwrap(),
253
253
  acceptor.clone(),
254
254
  ),
255
- Listener::Unix(listener) => TokioListener::Unix(
256
- TokioUnixListener::from_std(UnixListener::try_clone(listener).unwrap()).unwrap(),
257
- ),
255
+ Listener::Unix(listener) => {
256
+ TokioListener::Unix(TokioUnixListener::from_std(listener).unwrap())
257
+ }
258
258
  Listener::UnixTls((listener, acceptor)) => TokioListener::UnixTls(
259
- TokioUnixListener::from_std(UnixListener::try_clone(listener).unwrap()).unwrap(),
259
+ TokioUnixListener::from_std(listener).unwrap(),
260
260
  acceptor.clone(),
261
261
  ),
262
262
  }
@@ -53,8 +53,8 @@ impl ProcessWorker {
53
53
  }
54
54
  *self.child_pid.lock() = None;
55
55
  }
56
-
57
- match call_with_gvl(|_ruby| fork(cluster_template.server.after_fork.lock().clone())) {
56
+ match call_with_gvl(|_ruby| fork(cluster_template.server.hooks.get("after_fork").cloned()))
57
+ {
58
58
  Some(pid) => {
59
59
  *self.child_pid.lock() = Some(Pid::from_raw(pid));
60
60
  }
@@ -67,7 +67,7 @@ impl ProcessWorker {
67
67
  }
68
68
  match SingleMode::new(
69
69
  cluster_template.server.clone(),
70
- cluster_template.listeners.clone(),
70
+ cluster_template.listeners.lock().drain(..).collect(),
71
71
  cluster_template.lifecycle_channel.clone(),
72
72
  ) {
73
73
  Ok(single_mode) => {
@@ -83,6 +83,13 @@ impl ProcessWorker {
83
83
  Ok(())
84
84
  }
85
85
 
86
+ pub fn pid(&self) -> i32 {
87
+ if let Some(pid) = *self.child_pid.lock() {
88
+ return pid.as_raw();
89
+ }
90
+ 0
91
+ }
92
+
86
93
  pub(crate) fn memory_usage(&self) -> Option<u64> {
87
94
  if let Some(pid) = *self.child_pid.lock() {
88
95
  let s = System::new_all();
@@ -3,8 +3,11 @@ use crate::server::{
3
3
  process_worker::ProcessWorker,
4
4
  };
5
5
  use itsi_error::{ItsiError, Result};
6
- use itsi_rb_helpers::{call_without_gvl, create_ruby_thread};
6
+ use itsi_rb_helpers::{
7
+ call_proc_and_log_errors, call_with_gvl, call_without_gvl, create_ruby_thread,
8
+ };
7
9
  use itsi_tracing::{error, info, warn};
10
+ use magnus::Value;
8
11
  use nix::{
9
12
  libc::{self, exit},
10
13
  unistd::Pid,
@@ -19,9 +22,9 @@ use tokio::{
19
22
  sync::{broadcast, watch, Mutex},
20
23
  time::{self, sleep},
21
24
  };
22
- use tracing::instrument;
25
+ use tracing::{debug, instrument};
23
26
  pub(crate) struct ClusterMode {
24
- pub listeners: Arc<Vec<Arc<Listener>>>,
27
+ pub listeners: parking_lot::Mutex<Vec<Listener>>,
25
28
  pub server: Arc<Server>,
26
29
  pub process_workers: parking_lot::Mutex<Vec<ProcessWorker>>,
27
30
  pub lifecycle_channel: broadcast::Sender<LifecycleEvent>,
@@ -34,12 +37,9 @@ static CHILD_SIGNAL_SENDER: parking_lot::Mutex<Option<watch::Sender<()>>> =
34
37
  impl ClusterMode {
35
38
  pub fn new(
36
39
  server: Arc<Server>,
37
- listeners: Arc<Vec<Arc<Listener>>>,
40
+ listeners: Vec<Listener>,
38
41
  lifecycle_channel: broadcast::Sender<LifecycleEvent>,
39
42
  ) -> Self {
40
- if let Some(f) = server.before_fork.lock().take() {
41
- f();
42
- }
43
43
  let process_workers = (0..server.workers)
44
44
  .map(|_| ProcessWorker {
45
45
  worker_id: WORKER_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
@@ -48,7 +48,7 @@ impl ClusterMode {
48
48
  .collect();
49
49
 
50
50
  Self {
51
- listeners,
51
+ listeners: parking_lot::Mutex::new(listeners),
52
52
  server,
53
53
  process_workers: parking_lot::Mutex::new(process_workers),
54
54
  lifecycle_channel,
@@ -152,7 +152,7 @@ impl ClusterMode {
152
152
 
153
153
  tokio::select! {
154
154
  _ = monitor_handle => {
155
- info!("All children exited early, exit normally")
155
+ debug!("All children exited early, exit normally")
156
156
  }
157
157
  _ = sleep(Duration::from_secs_f64(shutdown_timeout)) => {
158
158
  warn!("Graceful shutdown timeout reached, force killing remaining children");
@@ -191,6 +191,9 @@ impl ClusterMode {
191
191
  #[instrument(skip(self), fields(mode = "cluster", pid=format!("{:?}", Pid::this())))]
192
192
  pub fn run(self: Arc<Self>) -> Result<()> {
193
193
  info!("Starting in Cluster mode");
194
+ if let Some(proc) = self.server.hooks.get("before_fork") {
195
+ call_with_gvl(|_| call_proc_and_log_errors(proc.clone()))
196
+ }
194
197
  self.process_workers
195
198
  .lock()
196
199
  .iter()
@@ -228,6 +231,9 @@ impl ClusterMode {
228
231
  if let Some(current_mem_usage) = largest_worker.memory_usage(){
229
232
  if current_mem_usage > memory_limit {
230
233
  largest_worker.reboot(self_ref.clone()).await.ok();
234
+ if let Some(hook) = self_ref.server.hooks.get("after_memory_threshold_reached") {
235
+ call_with_gvl(|_| hook.call::<_, Value>((largest_worker.pid(),)).ok() );
236
+ }
231
237
  }
232
238
  }
233
239
  }