itsi-server 0.1.11 → 0.1.12

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +7 -0
  4. data/Cargo.lock +1536 -45
  5. data/README.md +4 -0
  6. data/_index.md +6 -0
  7. data/exe/itsi +33 -74
  8. data/ext/itsi_error/src/lib.rs +9 -0
  9. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  10. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  11. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  12. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  13. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  14. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  15. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  22. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  23. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  24. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  25. data/ext/itsi_rb_helpers/src/lib.rs +34 -7
  26. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  27. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  28. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  29. data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  30. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  31. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  32. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  33. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  34. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  35. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  36. data/ext/itsi_server/Cargo.toml +69 -30
  37. data/ext/itsi_server/src/lib.rs +79 -147
  38. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  39. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +22 -3
  40. data/ext/itsi_server/src/ruby_types/itsi_grpc_request.rs +147 -0
  41. data/ext/itsi_server/src/ruby_types/itsi_grpc_response.rs +19 -0
  42. data/ext/itsi_server/src/ruby_types/itsi_grpc_stream/mod.rs +216 -0
  43. data/ext/itsi_server/src/{request/itsi_request.rs → ruby_types/itsi_http_request.rs} +101 -117
  44. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +72 -41
  45. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  46. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +355 -0
  47. data/ext/itsi_server/src/ruby_types/itsi_server.rs +82 -0
  48. data/ext/itsi_server/src/ruby_types/mod.rs +55 -0
  49. data/ext/itsi_server/src/server/bind.rs +13 -5
  50. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  51. data/ext/itsi_server/src/server/cache_store.rs +74 -0
  52. data/ext/itsi_server/src/server/itsi_service.rs +172 -0
  53. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  54. data/ext/itsi_server/src/server/listener.rs +102 -2
  55. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +153 -0
  56. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +47 -0
  57. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +58 -0
  58. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +82 -0
  59. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +321 -0
  60. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +139 -0
  61. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +300 -0
  62. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +287 -0
  63. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +48 -0
  64. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +127 -0
  65. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +191 -0
  66. data/ext/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +72 -0
  67. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +85 -0
  68. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +195 -0
  69. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  70. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +82 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +216 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +124 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +43 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +34 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +93 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +162 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +158 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  80. data/ext/itsi_server/src/server/middleware_stack/mod.rs +315 -0
  81. data/ext/itsi_server/src/server/mod.rs +8 -1
  82. data/ext/itsi_server/src/server/process_worker.rs +38 -12
  83. data/ext/itsi_server/src/server/rate_limiter.rs +565 -0
  84. data/ext/itsi_server/src/server/request_job.rs +11 -0
  85. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +119 -42
  86. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  87. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +256 -111
  88. data/ext/itsi_server/src/server/signal.rs +19 -0
  89. data/ext/itsi_server/src/server/static_file_server.rs +984 -0
  90. data/ext/itsi_server/src/server/thread_worker.rs +139 -94
  91. data/ext/itsi_server/src/server/types.rs +43 -0
  92. data/ext/itsi_tracing/Cargo.toml +1 -0
  93. data/ext/itsi_tracing/src/lib.rs +216 -45
  94. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  95. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  96. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  97. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  98. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  99. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  100. data/lib/itsi/{request.rb → http_request.rb} +29 -5
  101. data/lib/itsi/http_response.rb +39 -0
  102. data/lib/itsi/server/Itsi.rb +11 -19
  103. data/lib/itsi/server/config/dsl.rb +506 -0
  104. data/lib/itsi/server/config.rb +103 -8
  105. data/lib/itsi/server/default_app/default_app.rb +38 -0
  106. data/lib/itsi/server/grpc_interface.rb +213 -0
  107. data/lib/itsi/server/rack/handler/itsi.rb +8 -17
  108. data/lib/itsi/server/rack_interface.rb +23 -4
  109. data/lib/itsi/server/scheduler_interface.rb +1 -1
  110. data/lib/itsi/server/scheduler_mode.rb +4 -0
  111. data/lib/itsi/server/signal_trap.rb +7 -1
  112. data/lib/itsi/server/version.rb +1 -1
  113. data/lib/itsi/server.rb +74 -63
  114. data/lib/itsi/standard_headers.rb +86 -0
  115. metadata +84 -15
  116. data/ext/itsi_scheduler/extconf.rb +0 -6
  117. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  118. data/ext/itsi_server/src/request/mod.rs +0 -1
  119. data/ext/itsi_server/src/response/mod.rs +0 -1
  120. data/ext/itsi_server/src/server/itsi_server.rs +0 -288
  121. data/lib/itsi/server/options_dsl.rb +0 -401
  122. data/lib/itsi/stream_io.rb +0 -38
  123. /data/lib/itsi/{index.html → server/default_app/index.html} +0 -0
@@ -0,0 +1,355 @@
1
+ use super::file_watcher::{self};
2
+ use crate::{
3
+ ruby_types::ITSI_SERVER_CONFIG,
4
+ server::{bind::Bind, listener::Listener, middleware_stack::MiddlewareSet},
5
+ };
6
+ use derive_more::Debug;
7
+ use itsi_rb_helpers::{call_with_gvl, print_rb_backtrace, HeapVal, HeapValue};
8
+ use itsi_tracing::set_level;
9
+ use magnus::{
10
+ block::Proc,
11
+ error::Result,
12
+ value::{LazyId, ReprValue},
13
+ RArray, RHash, Ruby, Symbol, Value,
14
+ };
15
+ use nix::{
16
+ fcntl::{fcntl, FcntlArg, FdFlag},
17
+ unistd::{close, dup},
18
+ };
19
+ use parking_lot::{Mutex, RwLock};
20
+ use std::{
21
+ collections::HashMap,
22
+ os::fd::{AsRawFd, OwnedFd, RawFd},
23
+ path::PathBuf,
24
+ sync::{Arc, OnceLock},
25
+ };
26
+
27
+ static DEFAULT_BIND: &str = "http://localhost:3000";
28
+ static ID_BUILD_CONFIG: LazyId = LazyId::new("build_config");
29
+ static ID_RELOAD_EXEC: LazyId = LazyId::new("reload_exec");
30
+
31
+ #[derive(Debug, Clone)]
32
+ pub struct ItsiServerConfig {
33
+ pub cli_params: Arc<HeapValue<RHash>>,
34
+ pub itsifile_path: Option<PathBuf>,
35
+ pub itsi_config_proc: Arc<Option<HeapValue<Proc>>>,
36
+ #[debug(skip)]
37
+ pub server_params: Arc<RwLock<Arc<ServerParams>>>,
38
+ pub watcher_fd: Arc<Option<OwnedFd>>,
39
+ }
40
+
41
+ #[derive(Debug)]
42
+ pub struct ServerParams {
43
+ /// Cluster params
44
+ pub workers: u8,
45
+ pub worker_memory_limit: Option<u64>,
46
+ pub silence: bool,
47
+ pub shutdown_timeout: f64,
48
+ pub hooks: HashMap<String, HeapValue<Proc>>,
49
+ pub preload: bool,
50
+
51
+ pub notify_watchers: Option<Vec<(String, Vec<Vec<String>>)>>,
52
+ /// Worker params
53
+ pub threads: u8,
54
+ pub script_name: String,
55
+ pub streamable_body: bool,
56
+ pub multithreaded_reactor: bool,
57
+ pub scheduler_class: Option<String>,
58
+ pub oob_gc_responses_threshold: Option<u64>,
59
+ pub middleware_loader: HeapValue<Proc>,
60
+ pub default_app_loader: HeapValue<Proc>,
61
+ pub middleware: OnceLock<MiddlewareSet>,
62
+ pub binds: Vec<Bind>,
63
+ #[debug(skip)]
64
+ pub(crate) listeners: Mutex<Vec<Listener>>,
65
+ listener_info: Mutex<HashMap<String, i32>>,
66
+ }
67
+
68
+ impl ServerParams {
69
+ pub fn preload_ruby(self: &Arc<Self>) -> Result<()> {
70
+ call_with_gvl(|ruby| -> Result<()> {
71
+ if self
72
+ .scheduler_class
73
+ .as_ref()
74
+ .is_some_and(|t| t == "Itsi::Scheduler")
75
+ {
76
+ ruby.require("itsi/scheduler")?;
77
+ }
78
+ let default_app: HeapVal = self.default_app_loader.call::<_, Value>(())?.into();
79
+ let middleware = MiddlewareSet::new(
80
+ self.middleware_loader
81
+ .call::<_, Option<Value>>(())
82
+ .inspect_err(|e| {
83
+ if let Some(err_value) = e.value() {
84
+ print_rb_backtrace(err_value);
85
+ }
86
+ })?
87
+ .map(|mw| mw.into()),
88
+ default_app,
89
+ )?;
90
+ self.middleware.set(middleware).map_err(|_| {
91
+ magnus::Error::new(
92
+ magnus::exception::runtime_error(),
93
+ "Failed to set middleware",
94
+ )
95
+ })?;
96
+ Ok(())
97
+ })?;
98
+ Ok(())
99
+ }
100
+
101
+ fn from_rb_hash(rb_param_hash: RHash) -> Result<ServerParams> {
102
+ let workers = rb_param_hash
103
+ .fetch::<_, Option<u8>>("workers")?
104
+ .unwrap_or(num_cpus::get() as u8);
105
+ let worker_memory_limit: Option<u64> = rb_param_hash.fetch("worker_memory_limit")?;
106
+ let silence: bool = rb_param_hash.fetch("silence")?;
107
+ let multithreaded_reactor: bool = rb_param_hash.fetch("multithreaded_reactor")?;
108
+ let shutdown_timeout: f64 = rb_param_hash.fetch("shutdown_timeout")?;
109
+
110
+ let hooks: Option<RHash> = rb_param_hash.fetch("hooks")?;
111
+ let hooks = hooks
112
+ .map(|rhash| -> Result<HashMap<String, HeapValue<Proc>>> {
113
+ let mut hook_map: HashMap<String, HeapValue<Proc>> = HashMap::new();
114
+ for pair in rhash.enumeratorize::<_, ()>("each", ()) {
115
+ if let Some(pair_value) = RArray::from_value(pair?) {
116
+ if let (Ok(key), Ok(value)) =
117
+ (pair_value.entry::<Value>(0), pair_value.entry::<Proc>(1))
118
+ {
119
+ hook_map.insert(key.to_string(), HeapValue::from(value));
120
+ }
121
+ }
122
+ }
123
+ Ok(hook_map)
124
+ })
125
+ .transpose()?
126
+ .unwrap_or_default();
127
+ let preload: bool = rb_param_hash.fetch("preload")?;
128
+ let notify_watchers: Option<Vec<(String, Vec<Vec<String>>)>> =
129
+ rb_param_hash.fetch("notify_watchers")?;
130
+ let threads: u8 = rb_param_hash.fetch("threads")?;
131
+ let script_name: String = rb_param_hash.fetch("script_name")?;
132
+ let streamable_body: bool = rb_param_hash.fetch("streamable_body")?;
133
+ let scheduler_class: Option<String> = rb_param_hash.fetch("scheduler_class")?;
134
+ let oob_gc_responses_threshold: Option<u64> =
135
+ rb_param_hash.fetch("oob_gc_responses_threshold")?;
136
+ let middleware_loader: Proc = rb_param_hash.fetch("middleware_loader")?;
137
+ let default_app_loader: Proc = rb_param_hash.fetch("default_app_loader")?;
138
+ let log_level: Option<String> = rb_param_hash.fetch("log_level")?;
139
+
140
+ if let Some(level) = log_level {
141
+ set_level(&level);
142
+ }
143
+
144
+ let binds: Option<Vec<String>> = rb_param_hash.fetch("binds")?;
145
+ let binds = binds
146
+ .unwrap_or_else(|| vec![DEFAULT_BIND.to_string()])
147
+ .into_iter()
148
+ .map(|s| s.parse())
149
+ .collect::<itsi_error::Result<Vec<Bind>>>()?;
150
+
151
+ let listeners = if let Some(preexisting_listeners) =
152
+ rb_param_hash.delete::<_, Option<String>>("listeners")?
153
+ {
154
+ let bind_to_fd_map: HashMap<String, i32> = serde_json::from_str(&preexisting_listeners)
155
+ .map_err(|e| {
156
+ magnus::Error::new(
157
+ magnus::exception::exception(),
158
+ format!("Invalid listener info: {}", e),
159
+ )
160
+ })?;
161
+
162
+ binds
163
+ .iter()
164
+ .cloned()
165
+ .map(|bind| {
166
+ if let Some(fd) = bind_to_fd_map.get(&bind.listener_address_string()) {
167
+ Listener::inherit_fd(bind, *fd)
168
+ } else {
169
+ Listener::try_from(bind)
170
+ }
171
+ })
172
+ .collect::<std::result::Result<Vec<Listener>, _>>()?
173
+ .into_iter()
174
+ .collect::<Vec<_>>()
175
+ } else {
176
+ binds
177
+ .iter()
178
+ .cloned()
179
+ .map(Listener::try_from)
180
+ .collect::<std::result::Result<Vec<Listener>, _>>()?
181
+ .into_iter()
182
+ .collect::<Vec<_>>()
183
+ };
184
+
185
+ let listener_info = listeners
186
+ .iter()
187
+ .map(|listener| {
188
+ listener.handover().map_err(|e| {
189
+ magnus::Error::new(magnus::exception::runtime_error(), e.to_string())
190
+ })
191
+ })
192
+ .collect::<Result<HashMap<String, i32>>>()?;
193
+
194
+ Ok(ServerParams {
195
+ workers,
196
+ worker_memory_limit,
197
+ silence,
198
+ multithreaded_reactor,
199
+ shutdown_timeout,
200
+ hooks,
201
+ preload,
202
+ notify_watchers,
203
+ threads,
204
+ script_name,
205
+ streamable_body,
206
+ scheduler_class,
207
+ oob_gc_responses_threshold,
208
+ binds,
209
+ listener_info: Mutex::new(listener_info),
210
+ listeners: Mutex::new(listeners),
211
+ middleware_loader: middleware_loader.into(),
212
+ default_app_loader: default_app_loader.into(),
213
+ middleware: OnceLock::new(),
214
+ })
215
+ }
216
+ }
217
+
218
+ impl ItsiServerConfig {
219
+ pub fn new(
220
+ ruby: &Ruby,
221
+ cli_params: RHash,
222
+ itsifile_path: Option<PathBuf>,
223
+ itsi_config_proc: Option<Proc>,
224
+ ) -> Result<Self> {
225
+ let itsi_config_proc = Arc::new(itsi_config_proc.map(HeapValue::from));
226
+ let server_params = Self::combine_params(
227
+ ruby,
228
+ cli_params,
229
+ itsifile_path.as_ref(),
230
+ itsi_config_proc.clone(),
231
+ )?;
232
+ cli_params.delete::<_, Value>(Symbol::new("listeners"))?;
233
+
234
+ let watcher_fd = if let Some(watchers) = server_params.notify_watchers.clone() {
235
+ file_watcher::watch_groups(watchers)?
236
+ } else {
237
+ None
238
+ };
239
+
240
+ Ok(ItsiServerConfig {
241
+ cli_params: Arc::new(cli_params.into()),
242
+ server_params: RwLock::new(server_params.clone()).into(),
243
+ itsi_config_proc,
244
+ itsifile_path,
245
+ watcher_fd: watcher_fd.into(),
246
+ })
247
+ }
248
+
249
+ /// Reload
250
+ pub fn reload(self: Arc<Self>, cluster_worker: bool) -> Result<bool> {
251
+ let server_params = call_with_gvl(|ruby| {
252
+ Self::combine_params(
253
+ &ruby,
254
+ self.cli_params.cloned(),
255
+ self.itsifile_path.as_ref(),
256
+ self.itsi_config_proc.clone(),
257
+ )
258
+ })?;
259
+
260
+ let is_single_mode = self.server_params.read().workers == 1;
261
+
262
+ let requires_exec = if !is_single_mode && !server_params.preload {
263
+ // In cluster mode children are cycled during a reload
264
+ // and if preload is disabled, will get a clean memory slate,
265
+ // so we don't need to exec.
266
+ false
267
+ } else {
268
+ // In non-cluster mode, or when preloading is enabled, we shouldn't try to
269
+ // reload inside the existing process (as new code may conflict with old),
270
+ // and should re-exec instead.
271
+ true
272
+ };
273
+
274
+ *self.server_params.write() = server_params.clone();
275
+ Ok(requires_exec && (cluster_worker || is_single_mode))
276
+ }
277
+
278
+ fn combine_params(
279
+ ruby: &Ruby,
280
+ cli_params: RHash,
281
+ itsifile_path: Option<&PathBuf>,
282
+ itsi_config_proc: Arc<Option<HeapValue<Proc>>>,
283
+ ) -> Result<Arc<ServerParams>> {
284
+ let inner = itsi_config_proc
285
+ .as_ref()
286
+ .clone()
287
+ .map(|hv| hv.clone().inner());
288
+ let rb_param_hash: RHash = ruby.get_inner_ref(&ITSI_SERVER_CONFIG).funcall(
289
+ *ID_BUILD_CONFIG,
290
+ (cli_params, itsifile_path.cloned(), inner),
291
+ )?;
292
+ Ok(Arc::new(ServerParams::from_rb_hash(rb_param_hash)?))
293
+ }
294
+
295
+ fn clear_cloexec(fd: RawFd) -> nix::Result<()> {
296
+ let current_flags = fcntl(fd, FcntlArg::F_GETFD)?;
297
+ let mut flags = FdFlag::from_bits_truncate(current_flags);
298
+ // Remove the FD_CLOEXEC flag
299
+ flags.remove(FdFlag::FD_CLOEXEC);
300
+ // Set the new flags back on the file descriptor
301
+ fcntl(fd, FcntlArg::F_SETFD(flags))?;
302
+ Ok(())
303
+ }
304
+
305
+ pub fn dup_fds(self: &Arc<Self>) -> Result<()> {
306
+ let binding = self.server_params.read();
307
+ let mut listener_info_guard = binding.listener_info.lock();
308
+ let dupped_fd_map = listener_info_guard
309
+ .iter()
310
+ .map(|(str, fd)| {
311
+ let dupped_fd = dup(*fd).map_err(|errno| {
312
+ magnus::Error::new(
313
+ magnus::exception::exception(),
314
+ format!("Errno {} while trying to dup {}", errno, fd),
315
+ )
316
+ })?;
317
+ Self::clear_cloexec(dupped_fd).map_err(|e| {
318
+ magnus::Error::new(
319
+ magnus::exception::exception(),
320
+ format!("Failed to clear cloexec flag for fd {}: {}", dupped_fd, e),
321
+ )
322
+ })?;
323
+ Ok((str.clone(), dupped_fd))
324
+ })
325
+ .collect::<Result<HashMap<String, i32>>>()?;
326
+ *listener_info_guard = dupped_fd_map;
327
+ Ok(())
328
+ }
329
+
330
+ pub fn stop_watcher(self: &Arc<Self>) -> Result<()> {
331
+ if let Some(r_fd) = self.watcher_fd.as_ref() {
332
+ close(r_fd.as_raw_fd()).ok();
333
+ }
334
+ Ok(())
335
+ }
336
+
337
+ pub fn reload_exec(self: &Arc<Self>) -> Result<()> {
338
+ let listener_json =
339
+ serde_json::to_string(&self.server_params.read().listener_info.lock().clone())
340
+ .map_err(|e| {
341
+ magnus::Error::new(
342
+ magnus::exception::exception(),
343
+ format!("Invalid listener info: {}", e),
344
+ )
345
+ })?;
346
+
347
+ self.stop_watcher()?;
348
+ call_with_gvl(|ruby| -> Result<()> {
349
+ ruby.get_inner_ref(&ITSI_SERVER_CONFIG)
350
+ .funcall::<_, _, Value>(*ID_RELOAD_EXEC, (listener_json,))?;
351
+ Ok(())
352
+ })?;
353
+ Ok(())
354
+ }
355
+ }
@@ -0,0 +1,82 @@
1
+ use crate::server::{
2
+ serve_strategy::{cluster_mode::ClusterMode, single_mode::SingleMode, ServeStrategy},
3
+ signal::{clear_signal_handlers, reset_signal_handlers, send_shutdown_event},
4
+ };
5
+ use itsi_rb_helpers::{call_without_gvl, print_rb_backtrace};
6
+ use itsi_server_config::ItsiServerConfig;
7
+ use itsi_tracing::{error, run_silently};
8
+ use magnus::{block::Proc, error::Result, RHash, Ruby};
9
+ use parking_lot::Mutex;
10
+ use std::{path::PathBuf, sync::Arc};
11
+ use tracing::{info, instrument};
12
+ mod file_watcher;
13
+ pub mod itsi_server_config;
14
+ #[magnus::wrap(class = "Itsi::Server", free_immediately, size)]
15
+ #[derive(Clone)]
16
+ pub struct ItsiServer {
17
+ pub config: Arc<Mutex<Arc<ItsiServerConfig>>>,
18
+ }
19
+
20
+ impl ItsiServer {
21
+ pub fn new(
22
+ ruby: &Ruby,
23
+ cli_params: RHash,
24
+ itsifile_path: Option<PathBuf>,
25
+ itsi_config_proc: Option<Proc>,
26
+ ) -> Result<Self> {
27
+ Ok(Self {
28
+ config: Arc::new(Mutex::new(Arc::new(ItsiServerConfig::new(
29
+ ruby,
30
+ cli_params,
31
+ itsifile_path,
32
+ itsi_config_proc,
33
+ )?))),
34
+ })
35
+ }
36
+
37
+ pub fn stop(&self) -> Result<()> {
38
+ send_shutdown_event();
39
+ Ok(())
40
+ }
41
+
42
+ #[instrument(skip(self))]
43
+ pub fn start(&self) -> Result<()> {
44
+ let result = if self.config.lock().server_params.read().silence {
45
+ run_silently(|| self.build_and_run_strategy())
46
+ } else {
47
+ info!("Itsi - Rolling into action. 💨 ⚪ ");
48
+ self.build_and_run_strategy()
49
+ };
50
+ if let Err(e) = result {
51
+ if let Some(err_value) = e.value() {
52
+ print_rb_backtrace(err_value);
53
+ }
54
+ return Err(e);
55
+ }
56
+ Ok(())
57
+ }
58
+
59
+ pub(crate) fn build_strategy(&self) -> Result<ServeStrategy> {
60
+ let server_config = self.config.lock();
61
+ Ok(if server_config.server_params.read().workers > 1 {
62
+ ServeStrategy::Cluster(Arc::new(ClusterMode::new(server_config.clone())))
63
+ } else {
64
+ ServeStrategy::Single(Arc::new(SingleMode::new(server_config.clone())?))
65
+ })
66
+ }
67
+
68
+ fn build_and_run_strategy(&self) -> Result<()> {
69
+ reset_signal_handlers();
70
+ call_without_gvl(move || -> Result<()> {
71
+ let strategy = self.build_strategy()?;
72
+ if let Err(e) = strategy.clone().run() {
73
+ error!("Error running server: {}", e);
74
+ strategy.stop()?;
75
+ }
76
+ Ok(())
77
+ })?;
78
+ clear_signal_handlers();
79
+ info!("Server stopped");
80
+ Ok(())
81
+ }
82
+ }
@@ -0,0 +1,55 @@
1
+ use magnus::{value::Lazy, Module, RClass, RModule};
2
+
3
+ pub mod itsi_body_proxy;
4
+ pub mod itsi_grpc_request;
5
+ pub mod itsi_grpc_response;
6
+ pub mod itsi_grpc_stream;
7
+ pub mod itsi_http_request;
8
+ pub mod itsi_http_response;
9
+ pub mod itsi_server;
10
+
11
+ pub static ITSI_MODULE: Lazy<RModule> = Lazy::new(|ruby| ruby.define_module("Itsi").unwrap());
12
+ pub static ITSI_SERVER: Lazy<RClass> = Lazy::new(|ruby| {
13
+ ruby.get_inner(&ITSI_MODULE)
14
+ .define_class("Server", ruby.class_object())
15
+ .unwrap()
16
+ });
17
+
18
+ pub static ITSI_SERVER_CONFIG: Lazy<RModule> =
19
+ Lazy::new(|ruby| ruby.get_inner(&ITSI_SERVER).const_get("Config").unwrap());
20
+
21
+ pub static ITSI_REQUEST: Lazy<RClass> = Lazy::new(|ruby| {
22
+ ruby.get_inner(&ITSI_MODULE)
23
+ .define_class("HttpRequest", ruby.class_object())
24
+ .unwrap()
25
+ });
26
+
27
+ pub static ITSI_RESPONSE: Lazy<RClass> = Lazy::new(|ruby| {
28
+ ruby.get_inner(&ITSI_MODULE)
29
+ .define_class("HttpResponse", ruby.class_object())
30
+ .unwrap()
31
+ });
32
+
33
+ pub static ITSI_BODY_PROXY: Lazy<RClass> = Lazy::new(|ruby| {
34
+ ruby.get_inner(&ITSI_MODULE)
35
+ .define_class("BodyProxy", ruby.class_object())
36
+ .unwrap()
37
+ });
38
+
39
+ pub static ITSI_GRPC_REQUEST: Lazy<RClass> = Lazy::new(|ruby| {
40
+ ruby.get_inner(&ITSI_MODULE)
41
+ .define_class("GrpcRequest", ruby.class_object())
42
+ .unwrap()
43
+ });
44
+
45
+ pub static ITSI_GRPC_STREAM: Lazy<RClass> = Lazy::new(|ruby| {
46
+ ruby.get_inner(&ITSI_MODULE)
47
+ .define_class("GrpcStream", ruby.class_object())
48
+ .unwrap()
49
+ });
50
+
51
+ pub static ITSI_GRPC_RESPONSE: Lazy<RClass> = Lazy::new(|ruby| {
52
+ ruby.get_inner(&ITSI_MODULE)
53
+ .define_class("GrpcResponse", ruby.class_object())
54
+ .unwrap()
55
+ });
@@ -31,13 +31,21 @@ pub struct Bind {
31
31
  pub tls_config: Option<ItsiTlsAcceptor>,
32
32
  }
33
33
 
34
+ impl Bind {
35
+ pub fn listener_address_string(&self) -> String {
36
+ match &self.address {
37
+ BindAddress::Ip(ip) => format!("tcp://{}:{}", ip.to_canonical(), self.port.unwrap()),
38
+ BindAddress::UnixSocket(path) => {
39
+ format!("unix://{}", path.as_path().to_str().unwrap())
40
+ }
41
+ }
42
+ }
43
+ }
44
+
34
45
  impl std::fmt::Debug for Bind {
35
46
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36
47
  match &self.address {
37
48
  BindAddress::Ip(ip) => match self.protocol {
38
- BindProtocol::Unix | BindProtocol::Unixs => {
39
- write!(f, "{}://{}", self.protocol, ip)
40
- }
41
49
  BindProtocol::Https if self.port == Some(443) => {
42
50
  write!(f, "{}://{}", self.protocol, ip)
43
51
  }
@@ -158,8 +166,8 @@ fn resolve_hostname(hostname: &str) -> Option<IpAddr> {
158
166
  .to_socket_addrs()
159
167
  .ok()?
160
168
  .find_map(|addr| {
161
- if addr.is_ipv6() {
162
- Some(addr.ip()) // Prefer IPv6
169
+ if addr.is_ipv4() {
170
+ Some(addr.ip()) // Prefer IPv4
163
171
  } else {
164
172
  None
165
173
  }
@@ -0,0 +1,32 @@
1
+ use std::ops::Deref;
2
+
3
+ use bytes::Bytes;
4
+
5
+ #[derive(Debug)]
6
+ pub enum ByteFrame {
7
+ Data(Bytes),
8
+ End(Bytes),
9
+ Empty,
10
+ }
11
+
12
+ impl Deref for ByteFrame {
13
+ type Target = Bytes;
14
+
15
+ fn deref(&self) -> &Self::Target {
16
+ match self {
17
+ ByteFrame::Data(data) => data,
18
+ ByteFrame::End(data) => data,
19
+ ByteFrame::Empty => unreachable!(),
20
+ }
21
+ }
22
+ }
23
+
24
+ impl From<ByteFrame> for Bytes {
25
+ fn from(frame: ByteFrame) -> Self {
26
+ match frame {
27
+ ByteFrame::Data(data) => data,
28
+ ByteFrame::End(data) => data,
29
+ ByteFrame::Empty => unreachable!(),
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,74 @@
1
+ use async_trait::async_trait;
2
+ use redis::aio::ConnectionManager;
3
+ use redis::{Client, RedisError, Script};
4
+ use std::sync::Arc;
5
+ use std::time::Duration;
6
+
7
+ #[derive(Debug)]
8
+ pub enum CacheError {
9
+ RedisError(RedisError),
10
+ // Other error variants as needed.
11
+ }
12
+ /// A general-purpose cache trait with an atomic “increment with timeout” operation.
13
+ #[async_trait]
14
+ pub trait CacheStore: Send + Sync + std::fmt::Debug {
15
+ /// Increments the counter associated with `key` and sets (or extends) its expiration.
16
+ /// Returns the new counter value.
17
+ async fn increment(&self, key: &str, timeout: Duration) -> Result<u64, CacheError>;
18
+ }
19
+
20
+ /// A Redis-backed cache store using an async connection manager.
21
+ /// This uses a TLS-enabled connection when the URL is prefixed with "rediss://".
22
+ #[derive(Clone)]
23
+ pub struct RedisCacheStore {
24
+ connection: Arc<ConnectionManager>,
25
+ }
26
+
27
+ impl std::fmt::Debug for RedisCacheStore {
28
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29
+ f.debug_struct("RedisCacheStore").finish()
30
+ }
31
+ }
32
+
33
+ impl RedisCacheStore {
34
+ /// Constructs a new RedisCacheStore.
35
+ ///
36
+ /// Use a connection URL like "rediss://host:port" to enable TLS (with rustls under the hood).
37
+ /// This constructor is async because it sets up the connection manager.
38
+ pub async fn new(connection_url: &str) -> Result<Self, CacheError> {
39
+ let client = Client::open(connection_url).map_err(CacheError::RedisError)?;
40
+ let connection_manager = ConnectionManager::new(client)
41
+ .await
42
+ .map_err(CacheError::RedisError)?;
43
+ Ok(Self {
44
+ connection: Arc::new(connection_manager),
45
+ })
46
+ }
47
+ }
48
+
49
+ #[async_trait]
50
+ impl CacheStore for RedisCacheStore {
51
+ async fn increment(&self, key: &str, timeout: Duration) -> Result<u64, CacheError> {
52
+ let timeout_secs = timeout.as_secs();
53
+ // Lua script to:
54
+ // 1. INCR the key.
55
+ // 2. If the key doesn't have a TTL, set it.
56
+ let script = r#"
57
+ local current = redis.call('INCR', KEYS[1])
58
+ if redis.call('TTL', KEYS[1]) < 0 then
59
+ redis.call('EXPIRE', KEYS[1], ARGV[1])
60
+ end
61
+ return current
62
+ "#;
63
+ let script = Script::new(script);
64
+ // The ConnectionManager is cloneable and can be used concurrently.
65
+ let mut connection = (*self.connection).clone();
66
+ let value: i64 = script
67
+ .key(key)
68
+ .arg(timeout_secs)
69
+ .invoke_async(&mut connection)
70
+ .await
71
+ .map_err(CacheError::RedisError)?;
72
+ Ok(value as u64)
73
+ }
74
+ }