itsi-scheduler 0.1.5 → 0.1.19

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.

Potentially problematic release.


This version of itsi-scheduler might be problematic. Click here for more details.

Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/CODE_OF_CONDUCT.md +7 -0
  3. data/Cargo.lock +90 -22
  4. data/README.md +5 -0
  5. data/_index.md +7 -0
  6. data/ext/itsi_error/Cargo.toml +1 -0
  7. data/ext/itsi_error/src/lib.rs +106 -7
  8. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  9. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  10. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  11. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  12. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  13. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  14. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  15. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  22. data/ext/itsi_rb_helpers/Cargo.toml +1 -0
  23. data/ext/itsi_rb_helpers/src/heap_value.rs +18 -0
  24. data/ext/itsi_rb_helpers/src/lib.rs +59 -9
  25. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  26. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  27. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  28. 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
  29. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  30. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  31. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  32. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  33. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  34. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  35. data/ext/itsi_scheduler/src/itsi_scheduler.rs +1 -1
  36. data/ext/itsi_server/Cargo.lock +2956 -0
  37. data/ext/itsi_server/Cargo.toml +72 -28
  38. data/ext/itsi_server/src/default_responses/mod.rs +11 -0
  39. data/ext/itsi_server/src/env.rs +43 -0
  40. data/ext/itsi_server/src/lib.rs +113 -75
  41. data/ext/itsi_server/src/prelude.rs +2 -0
  42. data/ext/itsi_server/src/{body_proxy → ruby_types/itsi_body_proxy}/big_bytes.rs +10 -5
  43. data/ext/itsi_server/src/{body_proxy/itsi_body_proxy.rs → ruby_types/itsi_body_proxy/mod.rs} +29 -8
  44. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  45. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
  46. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +345 -0
  47. data/ext/itsi_server/src/{response/itsi_response.rs → ruby_types/itsi_http_response.rs} +84 -40
  48. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  49. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +375 -0
  50. data/ext/itsi_server/src/ruby_types/itsi_server.rs +83 -0
  51. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  52. data/ext/itsi_server/src/server/{bind.rs → binds/bind.rs} +56 -24
  53. data/ext/itsi_server/src/server/{listener.rs → binds/listener.rs} +218 -113
  54. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  55. data/ext/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +55 -17
  56. data/ext/itsi_server/src/server/{tls.rs → binds/tls.rs} +109 -28
  57. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  58. data/ext/itsi_server/src/server/http_message_types.rs +97 -0
  59. data/ext/itsi_server/src/server/io_stream.rs +2 -1
  60. data/ext/itsi_server/src/server/lifecycle_event.rs +3 -0
  61. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +165 -0
  62. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +56 -0
  63. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +87 -0
  64. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +86 -0
  65. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +285 -0
  66. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +142 -0
  67. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +289 -0
  68. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +292 -0
  69. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +55 -0
  70. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  71. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +157 -0
  72. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +195 -0
  73. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  74. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +201 -0
  75. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  76. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  77. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +87 -0
  78. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +414 -0
  79. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +131 -0
  80. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  81. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +44 -0
  82. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +36 -0
  83. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
  84. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +180 -0
  85. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  86. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +163 -0
  87. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  88. data/ext/itsi_server/src/server/middleware_stack/mod.rs +347 -0
  89. data/ext/itsi_server/src/server/mod.rs +6 -5
  90. data/ext/itsi_server/src/server/process_worker.rs +65 -14
  91. data/ext/itsi_server/src/server/request_job.rs +11 -0
  92. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +137 -49
  93. data/ext/itsi_server/src/server/serve_strategy/mod.rs +9 -6
  94. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +338 -164
  95. data/ext/itsi_server/src/server/signal.rs +32 -26
  96. data/ext/itsi_server/src/server/size_limited_incoming.rs +101 -0
  97. data/ext/itsi_server/src/server/thread_worker.rs +214 -107
  98. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  99. data/ext/itsi_server/src/services/itsi_http_service.rs +239 -0
  100. data/ext/itsi_server/src/services/mime_types.rs +1416 -0
  101. data/ext/itsi_server/src/services/mod.rs +6 -0
  102. data/ext/itsi_server/src/services/password_hasher.rs +83 -0
  103. data/ext/itsi_server/src/services/rate_limiter.rs +569 -0
  104. data/ext/itsi_server/src/services/static_file_server.rs +1324 -0
  105. data/ext/itsi_tracing/Cargo.toml +1 -0
  106. data/ext/itsi_tracing/src/lib.rs +312 -34
  107. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  108. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  109. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  110. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  111. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  112. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  113. data/lib/itsi/scheduler/version.rb +1 -1
  114. data/lib/itsi/scheduler.rb +2 -2
  115. metadata +93 -21
  116. data/ext/itsi_error/src/from.rs +0 -71
  117. data/ext/itsi_server/extconf.rb +0 -6
  118. data/ext/itsi_server/src/body_proxy/mod.rs +0 -2
  119. data/ext/itsi_server/src/request/itsi_request.rs +0 -277
  120. data/ext/itsi_server/src/request/mod.rs +0 -1
  121. data/ext/itsi_server/src/response/mod.rs +0 -1
  122. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -13
  123. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -5
  124. data/ext/itsi_server/src/server/itsi_server.rs +0 -244
  125. /data/ext/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
@@ -0,0 +1,344 @@
1
+ use super::itsi_grpc_response_stream::ItsiGrpcResponseStream;
2
+ use crate::prelude::*;
3
+ use crate::server::http_message_types::{HttpRequest, HttpResponse};
4
+ use crate::server::{byte_frame::ByteFrame, request_job::RequestJob};
5
+ use crate::services::itsi_http_service::HttpRequestContext;
6
+ use async_compression::futures::bufread::{GzipDecoder, GzipEncoder, ZlibDecoder, ZlibEncoder};
7
+ use bytes::Bytes;
8
+ use derive_more::Debug;
9
+ use futures::{executor::block_on, io::Cursor, AsyncReadExt};
10
+ use http::{request::Parts, Response, StatusCode};
11
+ use http_body_util::{combinators::BoxBody, BodyExt, Empty};
12
+ use itsi_error::CLIENT_CONNECTION_CLOSED;
13
+ use itsi_rb_helpers::{print_rb_backtrace, HeapValue};
14
+ use itsi_tracing::debug;
15
+ use magnus::{
16
+ block::Proc,
17
+ error::{ErrorType, Result as MagnusResult},
18
+ Error, Symbol,
19
+ };
20
+ use magnus::{
21
+ value::{LazyId, ReprValue},
22
+ Ruby, Value,
23
+ };
24
+ use regex::Regex;
25
+ use std::sync::LazyLock;
26
+ use std::{collections::HashMap, sync::Arc, time::Instant};
27
+ use tokio::sync::mpsc::{self};
28
+
29
+ static ID_MESSAGE: LazyId = LazyId::new("message");
30
+ static MIN_GZIP_SIZE: u32 = 128;
31
+ static MIN_DEFLATE_SIZE: u32 = 128;
32
+ static METHOD_NAME_REGEX: LazyLock<Regex> =
33
+ LazyLock::new(|| Regex::new(r"([a-z])([A-Z])").expect("Failed to compile regex"));
34
+
35
+ #[derive(Debug)]
36
+ #[magnus::wrap(class = "Itsi::GrpcCall", free_immediately, size)]
37
+ pub struct ItsiGrpcCall {
38
+ pub parts: Parts,
39
+ pub start: Instant,
40
+ pub compression_in: CompressionAlgorithm,
41
+ pub compression_out: CompressionAlgorithm,
42
+ #[debug(skip)]
43
+ pub context: HttpRequestContext,
44
+ #[debug(skip)]
45
+ pub stream: ItsiGrpcResponseStream,
46
+ }
47
+
48
+ #[derive(Debug, Clone)]
49
+ pub enum CompressionAlgorithm {
50
+ None,
51
+ Deflate,
52
+ Gzip,
53
+ }
54
+
55
+ impl ItsiGrpcCall {
56
+ pub fn service_name(&self) -> MagnusResult<String> {
57
+ let path = self.parts.uri.path();
58
+ Ok(path.split('/').nth_back(1).unwrap().to_string())
59
+ }
60
+
61
+ pub fn method_name(&self) -> MagnusResult<Symbol> {
62
+ let path = self.parts.uri.path();
63
+ let method_name = path.split('/').nth_back(0).unwrap();
64
+ let snake_case_method_name = METHOD_NAME_REGEX
65
+ .replace_all(method_name, "${1}_${2}")
66
+ .to_lowercase();
67
+ Ok(Symbol::new(snake_case_method_name))
68
+ }
69
+
70
+ pub fn stream(&self) -> MagnusResult<ItsiGrpcResponseStream> {
71
+ Ok(self.stream.clone())
72
+ }
73
+
74
+ pub fn timeout(&self) -> MagnusResult<Option<f64>> {
75
+ let timeout_str = self
76
+ .parts
77
+ .headers
78
+ .get("grpc-timeout")
79
+ .and_then(|hv| hv.to_str().ok())
80
+ .unwrap_or("");
81
+ Ok(parse_grpc_timeout(timeout_str).ok())
82
+ }
83
+
84
+ pub fn is_cancelled(&self) -> MagnusResult<bool> {
85
+ self.stream.is_cancelled()
86
+ }
87
+
88
+ pub fn add_headers(&self, headers: HashMap<Bytes, Vec<Bytes>>) -> MagnusResult<()> {
89
+ self.stream.add_headers(headers)
90
+ }
91
+
92
+ pub fn content_type_str(&self) -> &str {
93
+ self.parts
94
+ .headers
95
+ .get("Content-Type")
96
+ .and_then(|hv| hv.to_str().ok())
97
+ .unwrap_or("application/x-www-form-urlencoded")
98
+ }
99
+
100
+ pub fn is_json(&self) -> bool {
101
+ self.content_type_str() == "application/json"
102
+ }
103
+
104
+ pub fn process(self, ruby: &Ruby, app_proc: Arc<HeapValue<Proc>>) -> magnus::error::Result<()> {
105
+ let response = self.stream.clone();
106
+ let result = app_proc.call::<_, Value>((self,));
107
+ if let Err(err) = result {
108
+ Self::internal_error(ruby, response, err);
109
+ }
110
+ Ok(())
111
+ }
112
+
113
+ pub fn internal_error(_ruby: &Ruby, stream: ItsiGrpcResponseStream, err: Error) {
114
+ if let Some(rb_err) = err.value() {
115
+ print_rb_backtrace(rb_err);
116
+ stream.internal_server_error(err.to_string());
117
+ } else {
118
+ stream.internal_server_error(err.to_string());
119
+ }
120
+ }
121
+
122
+ pub(crate) async fn process_request(
123
+ app: Arc<HeapValue<Proc>>,
124
+ hyper_request: HttpRequest,
125
+ context: &HttpRequestContext,
126
+ nonblocking: bool,
127
+ ) -> itsi_error::Result<HttpResponse> {
128
+ let (request, mut receiver) = ItsiGrpcCall::new(hyper_request, context).await;
129
+ let shutdown_channel = context.service.shutdown_channel.clone();
130
+ let response_stream = request.stream.clone();
131
+ let sender = if nonblocking {
132
+ &context.nonblocking_sender
133
+ } else {
134
+ &context.sender
135
+ };
136
+ match sender
137
+ .send(RequestJob::ProcessGrpcRequest(request, app))
138
+ .await
139
+ {
140
+ Err(err) => {
141
+ error!("Error occurred: {}", err);
142
+ let mut response = Response::new(BoxBody::new(Empty::new()));
143
+ *response.status_mut() = StatusCode::BAD_REQUEST;
144
+ Ok(response)
145
+ }
146
+ _ => match receiver.recv().await {
147
+ Some(first_frame) => Ok(response_stream
148
+ .build_response(first_frame, receiver, shutdown_channel)
149
+ .await),
150
+ None => Ok(Response::new(BoxBody::new(Empty::new()))),
151
+ },
152
+ }
153
+ }
154
+
155
+ pub fn is_connection_closed_err(ruby: &Ruby, err: &Error) -> bool {
156
+ match err.error_type() {
157
+ ErrorType::Jump(_) => false,
158
+ ErrorType::Error(_, _) => false,
159
+ ErrorType::Exception(exception) => {
160
+ exception.is_kind_of(ruby.exception_eof_error())
161
+ && err
162
+ .value()
163
+ .map(|v| {
164
+ v.funcall::<_, _, String>(*ID_MESSAGE, ())
165
+ .unwrap_or("".to_string())
166
+ .eq(CLIENT_CONNECTION_CLOSED)
167
+ })
168
+ .unwrap_or(false)
169
+ }
170
+ }
171
+ }
172
+
173
+ pub fn should_compress_output(&self, message_size: u32) -> bool {
174
+ match self.compression_out {
175
+ CompressionAlgorithm::Gzip => message_size > MIN_GZIP_SIZE,
176
+ CompressionAlgorithm::Deflate => message_size > MIN_DEFLATE_SIZE,
177
+ CompressionAlgorithm::None => false,
178
+ }
179
+ }
180
+
181
+ pub fn compress_output(&self, bytes: Bytes) -> MagnusResult<Bytes> {
182
+ match self.compression_out {
183
+ CompressionAlgorithm::Gzip => Self::compress_gzip(bytes),
184
+ CompressionAlgorithm::Deflate => Self::compress_deflate(bytes),
185
+ CompressionAlgorithm::None => Ok(bytes),
186
+ }
187
+ }
188
+
189
+ pub fn decompress_input(&self, bytes: Bytes) -> MagnusResult<Bytes> {
190
+ match self.compression_in {
191
+ CompressionAlgorithm::Gzip => Self::decompress_gzip(bytes),
192
+ CompressionAlgorithm::Deflate => Self::decompress_deflate(bytes),
193
+ CompressionAlgorithm::None => Ok(bytes),
194
+ }
195
+ }
196
+
197
+ fn decompress_deflate(input: Bytes) -> MagnusResult<Bytes> {
198
+ let cursor = Cursor::new(input);
199
+ let mut decoder = ZlibDecoder::new(cursor);
200
+
201
+ let result = block_on(async {
202
+ let mut output = Vec::new();
203
+ decoder.read_to_end(&mut output).await?;
204
+ Ok(Bytes::from(output))
205
+ })
206
+ .map_err(|e: std::io::Error| {
207
+ Error::new(
208
+ magnus::exception::standard_error(),
209
+ format!("deflate decompression failed: {}", e),
210
+ )
211
+ })?;
212
+
213
+ Ok(result)
214
+ }
215
+
216
+ fn decompress_gzip(input: Bytes) -> MagnusResult<Bytes> {
217
+ let cursor = Cursor::new(input);
218
+ let mut decoder = GzipDecoder::new(cursor);
219
+
220
+ let result = block_on(async {
221
+ let mut output = Vec::new();
222
+ decoder.read_to_end(&mut output).await?;
223
+ Ok(Bytes::from(output))
224
+ })
225
+ .map_err(|e: std::io::Error| {
226
+ Error::new(
227
+ magnus::exception::standard_error(),
228
+ format!("gzip decompression failed: {}", e),
229
+ )
230
+ })?;
231
+
232
+ Ok(result)
233
+ }
234
+
235
+ fn compress_gzip(input: Bytes) -> MagnusResult<Bytes> {
236
+ let mut output = Vec::with_capacity(input.len() / 2);
237
+ let cursor = Cursor::new(input);
238
+ let mut encoder = GzipEncoder::new(cursor);
239
+
240
+ let result = block_on(async {
241
+ encoder.read_to_end(&mut output).await?;
242
+ Ok::<Bytes, std::io::Error>(output.into())
243
+ })
244
+ .map_err(|e| {
245
+ Error::new(
246
+ magnus::exception::standard_error(),
247
+ format!("gzip compression failed: {e}"),
248
+ )
249
+ })?;
250
+
251
+ Ok(result)
252
+ }
253
+
254
+ fn compress_deflate(input: Bytes) -> MagnusResult<Bytes> {
255
+ let mut output = Vec::with_capacity(input.len() / 2);
256
+ let cursor = Cursor::new(input);
257
+ let mut encoder = ZlibEncoder::new(cursor);
258
+
259
+ let result = block_on(async {
260
+ encoder.read_to_end(&mut output).await?;
261
+ Ok::<Bytes, std::io::Error>(output.into())
262
+ })
263
+ .map_err(|e| {
264
+ Error::new(
265
+ magnus::exception::standard_error(),
266
+ format!("deflate compression failed: {e}"),
267
+ )
268
+ })?;
269
+
270
+ Ok(result)
271
+ }
272
+
273
+ pub(crate) async fn new(
274
+ request: HttpRequest,
275
+ context: &HttpRequestContext,
276
+ ) -> (ItsiGrpcCall, mpsc::Receiver<ByteFrame>) {
277
+ let (parts, body) = request.into_parts();
278
+ let response_channel = mpsc::channel::<ByteFrame>(100);
279
+ let compression_in: CompressionAlgorithm = match parts.headers.get("grpc-encoding") {
280
+ Some(encoding) => match encoding.to_str() {
281
+ Ok(encoding) => match encoding {
282
+ "gzip" => CompressionAlgorithm::Gzip,
283
+ "deflate" => CompressionAlgorithm::Deflate,
284
+ _ => CompressionAlgorithm::None,
285
+ },
286
+ Err(_) => CompressionAlgorithm::None,
287
+ },
288
+ None => CompressionAlgorithm::None,
289
+ };
290
+ let compression_out: CompressionAlgorithm = match parts.headers.get("grpc-accept-encoding")
291
+ {
292
+ Some(accept_encoding) => match accept_encoding.to_str() {
293
+ Ok(accept_encoding) => {
294
+ let encodings: Vec<&str> =
295
+ accept_encoding.split(',').map(|s| s.trim()).collect();
296
+ if encodings.contains(&"gzip") {
297
+ CompressionAlgorithm::Gzip
298
+ } else if encodings.contains(&"deflate") {
299
+ CompressionAlgorithm::Deflate
300
+ } else {
301
+ CompressionAlgorithm::None
302
+ }
303
+ }
304
+ Err(_) => CompressionAlgorithm::None,
305
+ },
306
+ None => CompressionAlgorithm::None,
307
+ };
308
+ (
309
+ Self {
310
+ context: context.clone(),
311
+ start: Instant::now(),
312
+ compression_out: compression_out.clone(),
313
+ compression_in,
314
+ parts,
315
+ stream: ItsiGrpcResponseStream::new(
316
+ compression_out,
317
+ response_channel.0,
318
+ body.into_data_stream(),
319
+ )
320
+ .await,
321
+ },
322
+ response_channel.1,
323
+ )
324
+ }
325
+ }
326
+
327
+ fn parse_grpc_timeout(timeout_str: &str) -> Result<f64> {
328
+ if timeout_str.len() < 2 {
329
+ return Err("Timeout string too short".into());
330
+ }
331
+ let (value_str, unit) = timeout_str.split_at(timeout_str.len() - 1);
332
+ let value: u64 = value_str.parse().map_err(|_| "Invalid timeout value")?;
333
+ let duration_secs = match unit {
334
+ "n" => value as f64 / 1_000_000_000.0, // nanoseconds
335
+ "u" => value as f64 / 1_000_000.0, // microseconds
336
+ "m" => value as f64 / 1_000.0, // milliseconds
337
+ "S" => value as f64, // seconds
338
+ "M" => value as f64 * 60.0, // minutes
339
+ "H" => value as f64 * 3600.0, // hours
340
+ _ => return Err("Invalid timeout unit".into()),
341
+ };
342
+
343
+ Ok(duration_secs)
344
+ }
@@ -0,0 +1,264 @@
1
+ use super::itsi_grpc_call::CompressionAlgorithm;
2
+ use crate::prelude::*;
3
+ use crate::server::http_message_types::HttpResponse;
4
+ use crate::server::size_limited_incoming::SizeLimitedIncoming;
5
+ use crate::server::{byte_frame::ByteFrame, serve_strategy::single_mode::RunningPhase};
6
+ use bytes::Bytes;
7
+ use derive_more::Debug;
8
+ use futures::stream::unfold;
9
+ use http::Version;
10
+ use http::{
11
+ header::{HeaderName, HeaderValue},
12
+ HeaderMap, Response,
13
+ };
14
+ use http_body_util::{combinators::BoxBody, BodyDataStream, BodyExt, Empty, Full, StreamBody};
15
+ use hyper::body::{Frame, Incoming};
16
+ use magnus::error::Result as MagnusResult;
17
+ use nix::unistd::pipe;
18
+ use parking_lot::Mutex;
19
+ use std::sync::atomic::{AtomicBool, Ordering};
20
+ use std::{
21
+ collections::HashMap,
22
+ os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd},
23
+ sync::Arc,
24
+ };
25
+ use tokio::{
26
+ spawn,
27
+ sync::{
28
+ mpsc::{self, Sender},
29
+ oneshot, watch,
30
+ },
31
+ };
32
+ use tokio_stream::{wrappers::ReceiverStream, StreamExt};
33
+
34
+ #[derive(Debug, Clone)]
35
+ #[magnus::wrap(class = "Itsi::GrpcResponseStream", free_immediately, size)]
36
+ pub struct ItsiGrpcResponseStream {
37
+ pub inner: Arc<Mutex<ItsiGrpcResponseStreamInner>>,
38
+ pub cancelled: Arc<AtomicBool>,
39
+ }
40
+
41
+ #[derive(Debug)]
42
+ pub struct ItsiGrpcResponseStreamInner {
43
+ pub incoming_reader: Option<OwnedFd>,
44
+ pub buf: Vec<u8>,
45
+ pub response_sender: Sender<ByteFrame>,
46
+ pub response: Option<HttpResponse>,
47
+ pub response_headers: HeaderMap,
48
+ trailer_tx: oneshot::Sender<HeaderMap>,
49
+ trailer_rx: Option<oneshot::Receiver<HeaderMap>>,
50
+ }
51
+
52
+ impl ItsiGrpcResponseStreamInner {
53
+ pub fn reader(&mut self) -> MagnusResult<i32> {
54
+ Ok(self.incoming_reader.take().unwrap().into_raw_fd())
55
+ }
56
+
57
+ pub fn write(&mut self, bytes: Bytes) -> MagnusResult<()> {
58
+ self.response_sender
59
+ .blocking_send(ByteFrame::Data(bytes))
60
+ .map_err(|err| {
61
+ magnus::Error::new(
62
+ magnus::exception::io_error(),
63
+ format!("Trying to write to closed stream: {:?}", err),
64
+ )
65
+ })?;
66
+ Ok(())
67
+ }
68
+
69
+ pub fn flush(&mut self) -> MagnusResult<()> {
70
+ Ok(())
71
+ }
72
+
73
+ pub fn send_trailers(&mut self, trailers: HashMap<String, String>) -> MagnusResult<()> {
74
+ let mut header_map = HeaderMap::new();
75
+ for (key, value) in trailers {
76
+ if let (Ok(hn), Ok(hv)) = (key.parse::<HeaderName>(), value.parse::<HeaderValue>()) {
77
+ header_map.insert(hn, hv);
78
+ }
79
+ }
80
+ let trailer_tx = std::mem::replace(&mut self.trailer_tx, oneshot::channel().0);
81
+ trailer_tx.send(header_map).map_err(|err| {
82
+ magnus::Error::new(
83
+ magnus::exception::standard_error(),
84
+ format!("Error sending trailers {:?}", err),
85
+ )
86
+ })?;
87
+ Ok(())
88
+ }
89
+
90
+ pub fn close(&mut self) -> MagnusResult<()> {
91
+ self.response_sender.blocking_send(ByteFrame::Empty).ok();
92
+ Ok(())
93
+ }
94
+
95
+ pub fn add_headers(&mut self, headers: HashMap<Bytes, Vec<Bytes>>) -> MagnusResult<()> {
96
+ for (name, values) in headers {
97
+ let header_name = HeaderName::from_bytes(&name).map_err(|e| {
98
+ itsi_error::ItsiError::InvalidInput(format!(
99
+ "Invalid header name {:?}: {:?}",
100
+ name, e
101
+ ))
102
+ })?;
103
+ for value in values {
104
+ let header_value = unsafe { HeaderValue::from_maybe_shared_unchecked(value) };
105
+ self.response_headers.insert(&header_name, header_value);
106
+ }
107
+ }
108
+
109
+ Ok(())
110
+ }
111
+ }
112
+
113
+ impl ItsiGrpcResponseStream {
114
+ pub async fn new(
115
+ compression_out: CompressionAlgorithm,
116
+ response_sender: Sender<ByteFrame>,
117
+ mut body: BodyDataStream<SizeLimitedIncoming<Incoming>>,
118
+ ) -> Self {
119
+ let (trailer_tx, trailer_rx) = oneshot::channel::<HeaderMap>();
120
+ let (pipe_read, pipe_write) = pipe().unwrap();
121
+
122
+ nix::fcntl::fcntl(
123
+ pipe_read.as_raw_fd(),
124
+ nix::fcntl::FcntlArg::F_SETFL(nix::fcntl::OFlag::O_NONBLOCK),
125
+ )
126
+ .unwrap();
127
+
128
+ nix::fcntl::fcntl(
129
+ pipe_write.as_raw_fd(),
130
+ nix::fcntl::FcntlArg::F_SETFL(nix::fcntl::OFlag::O_NONBLOCK),
131
+ )
132
+ .unwrap();
133
+
134
+ let pipe_raw_fd = pipe_write.into_raw_fd();
135
+
136
+ let cancelled = Arc::new(AtomicBool::new(false));
137
+ let cancelled_clone = cancelled.clone();
138
+ spawn(async move {
139
+ use std::io::Write;
140
+ let mut write_end = unsafe { std::fs::File::from_raw_fd(pipe_raw_fd) };
141
+ while let Some(Ok(body)) = body.next().await {
142
+ write_end.write_all(&body).unwrap();
143
+ }
144
+ cancelled_clone.store(true, Ordering::SeqCst);
145
+ });
146
+
147
+ let mut response_headers = HeaderMap::new();
148
+
149
+ match compression_out {
150
+ CompressionAlgorithm::None => (),
151
+ CompressionAlgorithm::Deflate => {
152
+ response_headers.insert("grpc-encoding", "deflate".parse().unwrap());
153
+ }
154
+ CompressionAlgorithm::Gzip => {
155
+ response_headers.insert("grpc-encoding", "gzip".parse().unwrap());
156
+ }
157
+ }
158
+ ItsiGrpcResponseStream {
159
+ inner: Arc::new(Mutex::new(ItsiGrpcResponseStreamInner {
160
+ buf: Vec::new(),
161
+ response_headers,
162
+ incoming_reader: Some(pipe_read),
163
+ response_sender,
164
+ response: Some(Response::new(BoxBody::new(Empty::new()))),
165
+ trailer_tx,
166
+ trailer_rx: Some(trailer_rx),
167
+ })),
168
+ cancelled,
169
+ }
170
+ }
171
+
172
+ pub fn reader(&self) -> MagnusResult<i32> {
173
+ self.inner.lock().reader()
174
+ }
175
+
176
+ pub fn write(&self, bytes: Bytes) -> MagnusResult<()> {
177
+ self.inner.lock().write(bytes)
178
+ }
179
+
180
+ pub fn flush(&self) -> MagnusResult<()> {
181
+ self.inner.lock().flush()
182
+ }
183
+
184
+ pub fn is_cancelled(&self) -> MagnusResult<bool> {
185
+ Ok(self.cancelled.load(Ordering::SeqCst))
186
+ }
187
+
188
+ pub fn send_trailers(&self, trailers: HashMap<String, String>) -> MagnusResult<()> {
189
+ self.inner.lock().send_trailers(trailers)
190
+ }
191
+
192
+ pub fn close(&self) -> MagnusResult<()> {
193
+ self.inner.lock().close()
194
+ }
195
+
196
+ pub fn add_headers(&self, headers: HashMap<Bytes, Vec<Bytes>>) -> MagnusResult<()> {
197
+ self.inner.lock().add_headers(headers)
198
+ }
199
+
200
+ pub async fn build_response(
201
+ &self,
202
+ first_frame: ByteFrame,
203
+ receiver: mpsc::Receiver<ByteFrame>,
204
+ shutdown_rx: watch::Receiver<RunningPhase>,
205
+ ) -> HttpResponse {
206
+ let mut response = self.inner.lock().response.take().unwrap();
207
+ let rx = self.inner.lock().trailer_rx.take().unwrap();
208
+ *response.version_mut() = Version::HTTP_2;
209
+ *response.headers_mut() = self.inner.lock().response_headers.clone();
210
+ *response.body_mut() = if matches!(first_frame, ByteFrame::Empty) {
211
+ BoxBody::new(Empty::new())
212
+ } else if matches!(first_frame, ByteFrame::End(_)) {
213
+ BoxBody::new(Full::new(first_frame.into()))
214
+ } else {
215
+ let initial_frame = tokio_stream::once(Ok(Frame::data(Bytes::from(first_frame))));
216
+ let frame_stream = unfold(
217
+ (ReceiverStream::new(receiver), shutdown_rx),
218
+ |(mut receiver, mut shutdown_rx)| async move {
219
+ if let RunningPhase::ShutdownPending = *shutdown_rx.borrow() {
220
+ return None;
221
+ }
222
+ loop {
223
+ tokio::select! {
224
+ maybe_bytes = receiver.next() => {
225
+ match maybe_bytes {
226
+ Some(ByteFrame::Data(bytes)) | Some(ByteFrame::End(bytes)) => {
227
+ return Some((Ok(Frame::data(bytes)), (receiver, shutdown_rx)));
228
+ }
229
+ _ => {
230
+ return None;
231
+ }
232
+ }
233
+ },
234
+ _ = shutdown_rx.changed() => {
235
+ match *shutdown_rx.borrow() {
236
+ RunningPhase::ShutdownPending => {
237
+ warn!("Disconnecting streaming client.");
238
+ return None;
239
+ },
240
+ _ => continue,
241
+ }
242
+ }
243
+ }
244
+ }
245
+ },
246
+ );
247
+
248
+ let combined_stream = initial_frame.chain(frame_stream);
249
+ BoxBody::new(StreamBody::new(combined_stream))
250
+ }
251
+ .with_trailers(async move {
252
+ match rx.await {
253
+ Ok(trailers) => Some(Ok(trailers)),
254
+ Err(_err) => None,
255
+ }
256
+ })
257
+ .boxed();
258
+ response
259
+ }
260
+
261
+ pub fn internal_server_error(&self, message: String) {
262
+ error!(message);
263
+ }
264
+ }