itsi 0.2.16 → 0.2.18

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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/.zed/settings.json +32 -0
  3. data/CHANGELOG.md +21 -0
  4. data/Cargo.lock +4 -2
  5. data/crates/itsi_acme/Cargo.toml +1 -1
  6. data/crates/itsi_scheduler/Cargo.toml +1 -1
  7. data/crates/itsi_server/Cargo.toml +3 -1
  8. data/crates/itsi_server/src/lib.rs +6 -1
  9. data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +2 -0
  10. data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +4 -4
  11. data/crates/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +14 -13
  12. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +64 -33
  13. data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +151 -152
  14. data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +422 -110
  15. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +62 -15
  16. data/crates/itsi_server/src/ruby_types/itsi_server.rs +1 -1
  17. data/crates/itsi_server/src/server/binds/listener.rs +45 -7
  18. data/crates/itsi_server/src/server/frame_stream.rs +142 -0
  19. data/crates/itsi_server/src/server/http_message_types.rs +142 -9
  20. data/crates/itsi_server/src/server/io_stream.rs +28 -5
  21. data/crates/itsi_server/src/server/lifecycle_event.rs +1 -1
  22. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +2 -3
  23. data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +8 -10
  24. data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +2 -3
  25. data/crates/itsi_server/src/server/middleware_stack/middlewares/csp.rs +3 -3
  26. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +54 -56
  27. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +5 -7
  28. data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +5 -5
  29. data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +7 -10
  30. data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +2 -3
  31. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +1 -2
  32. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +4 -6
  33. data/crates/itsi_server/src/server/mod.rs +1 -0
  34. data/crates/itsi_server/src/server/process_worker.rs +3 -4
  35. data/crates/itsi_server/src/server/serve_strategy/acceptor.rs +16 -12
  36. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +83 -31
  37. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +166 -142
  38. data/crates/itsi_server/src/server/signal.rs +37 -9
  39. data/crates/itsi_server/src/server/thread_worker.rs +84 -69
  40. data/crates/itsi_server/src/services/itsi_http_service.rs +43 -43
  41. data/crates/itsi_server/src/services/static_file_server.rs +28 -47
  42. data/docs/benchmark-dashboard/.gitignore +27 -0
  43. data/docs/benchmark-dashboard/app/api/benchmarks/route.ts +22 -0
  44. data/docs/benchmark-dashboard/app/globals.css +94 -0
  45. data/docs/benchmark-dashboard/app/layout.tsx +20 -0
  46. data/docs/benchmark-dashboard/app/page.tsx +252 -0
  47. data/docs/benchmark-dashboard/components/benchmark-dashboard.tsx +1663 -0
  48. data/docs/benchmark-dashboard/components/theme-provider.tsx +11 -0
  49. data/docs/benchmark-dashboard/components/ui/accordion.tsx +58 -0
  50. data/docs/benchmark-dashboard/components/ui/alert-dialog.tsx +141 -0
  51. data/docs/benchmark-dashboard/components/ui/alert.tsx +59 -0
  52. data/docs/benchmark-dashboard/components/ui/aspect-ratio.tsx +7 -0
  53. data/docs/benchmark-dashboard/components/ui/avatar.tsx +50 -0
  54. data/docs/benchmark-dashboard/components/ui/badge.tsx +36 -0
  55. data/docs/benchmark-dashboard/components/ui/breadcrumb.tsx +115 -0
  56. data/docs/benchmark-dashboard/components/ui/button.tsx +56 -0
  57. data/docs/benchmark-dashboard/components/ui/calendar.tsx +66 -0
  58. data/docs/benchmark-dashboard/components/ui/card.tsx +79 -0
  59. data/docs/benchmark-dashboard/components/ui/carousel.tsx +262 -0
  60. data/docs/benchmark-dashboard/components/ui/chart.tsx +365 -0
  61. data/docs/benchmark-dashboard/components/ui/checkbox.tsx +30 -0
  62. data/docs/benchmark-dashboard/components/ui/collapsible.tsx +11 -0
  63. data/docs/benchmark-dashboard/components/ui/command.tsx +153 -0
  64. data/docs/benchmark-dashboard/components/ui/context-menu.tsx +200 -0
  65. data/docs/benchmark-dashboard/components/ui/dialog.tsx +122 -0
  66. data/docs/benchmark-dashboard/components/ui/drawer.tsx +118 -0
  67. data/docs/benchmark-dashboard/components/ui/dropdown-menu.tsx +200 -0
  68. data/docs/benchmark-dashboard/components/ui/form.tsx +178 -0
  69. data/docs/benchmark-dashboard/components/ui/hover-card.tsx +29 -0
  70. data/docs/benchmark-dashboard/components/ui/input-otp.tsx +71 -0
  71. data/docs/benchmark-dashboard/components/ui/input.tsx +22 -0
  72. data/docs/benchmark-dashboard/components/ui/label.tsx +26 -0
  73. data/docs/benchmark-dashboard/components/ui/loading-spinner.tsx +12 -0
  74. data/docs/benchmark-dashboard/components/ui/menubar.tsx +236 -0
  75. data/docs/benchmark-dashboard/components/ui/navigation-menu.tsx +128 -0
  76. data/docs/benchmark-dashboard/components/ui/pagination.tsx +117 -0
  77. data/docs/benchmark-dashboard/components/ui/popover.tsx +31 -0
  78. data/docs/benchmark-dashboard/components/ui/progress.tsx +28 -0
  79. data/docs/benchmark-dashboard/components/ui/radio-group.tsx +44 -0
  80. data/docs/benchmark-dashboard/components/ui/resizable.tsx +45 -0
  81. data/docs/benchmark-dashboard/components/ui/scroll-area.tsx +48 -0
  82. data/docs/benchmark-dashboard/components/ui/select.tsx +160 -0
  83. data/docs/benchmark-dashboard/components/ui/separator.tsx +31 -0
  84. data/docs/benchmark-dashboard/components/ui/sheet.tsx +140 -0
  85. data/docs/benchmark-dashboard/components/ui/sidebar.tsx +763 -0
  86. data/docs/benchmark-dashboard/components/ui/skeleton.tsx +15 -0
  87. data/docs/benchmark-dashboard/components/ui/slider.tsx +28 -0
  88. data/docs/benchmark-dashboard/components/ui/sonner.tsx +31 -0
  89. data/docs/benchmark-dashboard/components/ui/switch.tsx +29 -0
  90. data/docs/benchmark-dashboard/components/ui/table.tsx +117 -0
  91. data/docs/benchmark-dashboard/components/ui/tabs.tsx +55 -0
  92. data/docs/benchmark-dashboard/components/ui/textarea.tsx +22 -0
  93. data/docs/benchmark-dashboard/components/ui/toast.tsx +129 -0
  94. data/docs/benchmark-dashboard/components/ui/toaster.tsx +35 -0
  95. data/docs/benchmark-dashboard/components/ui/toggle-group.tsx +61 -0
  96. data/docs/benchmark-dashboard/components/ui/toggle.tsx +45 -0
  97. data/docs/benchmark-dashboard/components/ui/tooltip.tsx +30 -0
  98. data/docs/benchmark-dashboard/components/ui/use-mobile.tsx +19 -0
  99. data/docs/benchmark-dashboard/components/ui/use-toast.ts +194 -0
  100. data/docs/benchmark-dashboard/components.json +21 -0
  101. data/docs/benchmark-dashboard/dist/benchmark-dashboard.css +1 -0
  102. data/docs/benchmark-dashboard/dist/benchmark-dashboard.iife.js +211 -0
  103. data/docs/benchmark-dashboard/dist/placeholder-logo.png +0 -0
  104. data/docs/benchmark-dashboard/dist/placeholder-logo.svg +1 -0
  105. data/docs/benchmark-dashboard/dist/placeholder-user.jpg +0 -0
  106. data/docs/benchmark-dashboard/dist/placeholder.jpg +0 -0
  107. data/docs/benchmark-dashboard/dist/placeholder.svg +1 -0
  108. data/docs/benchmark-dashboard/embed.tsx +13 -0
  109. data/docs/benchmark-dashboard/hooks/use-mobile.tsx +19 -0
  110. data/docs/benchmark-dashboard/hooks/use-toast.ts +194 -0
  111. data/docs/benchmark-dashboard/lib/benchmark-utils.ts +54 -0
  112. data/docs/benchmark-dashboard/lib/utils.ts +6 -0
  113. data/docs/benchmark-dashboard/next.config.mjs +14 -0
  114. data/docs/benchmark-dashboard/package-lock.json +5859 -0
  115. data/docs/benchmark-dashboard/package.json +72 -0
  116. data/docs/benchmark-dashboard/pnpm-lock.yaml +5 -0
  117. data/docs/benchmark-dashboard/postcss.config.mjs +8 -0
  118. data/docs/benchmark-dashboard/styles/globals.css +94 -0
  119. data/docs/benchmark-dashboard/tailwind.config.ts +96 -0
  120. data/docs/benchmark-dashboard/tsconfig.json +27 -0
  121. data/docs/benchmark-dashboard/vite.config.ts +24 -0
  122. data/docs/build.rb +52 -0
  123. data/docs/content/acknowledgements/_index.md +1 -1
  124. data/docs/content/benchmarks/index.md +96 -0
  125. data/docs/content/configuration/_index.md +2 -2
  126. data/docs/content/getting_started/_index.md +76 -46
  127. data/docs/content/itsi_scheduler/_index.md +2 -2
  128. data/docs/hugo.yaml +10 -1
  129. data/docs/static/results.json +1 -0
  130. data/docs/static/scripts/benchmark-dashboard.iife.js +211 -0
  131. data/docs/static/styles/benchmark-dashboard.css +1 -0
  132. data/examples/api_with_schema_and_controllers/README.md +1 -1
  133. data/gems/scheduler/Cargo.lock +1 -1
  134. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  135. data/gems/server/Cargo.lock +3 -1
  136. data/gems/server/exe/itsi +8 -5
  137. data/gems/server/lib/itsi/http_request.rb +31 -39
  138. data/gems/server/lib/itsi/http_response.rb +5 -0
  139. data/gems/server/lib/itsi/rack_env_pool.rb +59 -0
  140. data/gems/server/lib/itsi/server/config/dsl.rb +6 -4
  141. data/gems/server/lib/itsi/server/config/middleware/compression.md +3 -3
  142. data/gems/server/lib/itsi/server/config/middleware/endpoint/controller.md +1 -1
  143. data/gems/server/lib/itsi/server/config/middleware/proxy.md +2 -2
  144. data/gems/server/lib/itsi/server/config/middleware/proxy.rb +1 -1
  145. data/gems/server/lib/itsi/server/config/middleware/rackup_file.rb +2 -2
  146. data/gems/server/lib/itsi/server/config/options/auto_reload_config.rb +11 -6
  147. data/gems/server/lib/itsi/server/config/options/include.md +1 -0
  148. data/gems/server/lib/itsi/server/config/options/include.rb +13 -8
  149. data/gems/server/lib/itsi/server/config/options/pipeline_flush.md +16 -0
  150. data/gems/server/lib/itsi/server/config/options/pipeline_flush.rb +19 -0
  151. data/gems/server/lib/itsi/server/config/options/reuse_port.rb +2 -4
  152. data/gems/server/lib/itsi/server/config/options/writev.md +25 -0
  153. data/gems/server/lib/itsi/server/config/options/writev.rb +19 -0
  154. data/gems/server/lib/itsi/server/config.rb +22 -9
  155. data/gems/server/lib/itsi/server/default_config/Itsi.rb +9 -8
  156. data/gems/server/lib/itsi/server/grpc/grpc_call.rb +2 -0
  157. data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +2 -2
  158. data/gems/server/lib/itsi/server/rack/handler/itsi.rb +3 -1
  159. data/gems/server/lib/itsi/server/rack_interface.rb +17 -12
  160. data/gems/server/lib/itsi/server/scheduler_interface.rb +2 -0
  161. data/gems/server/lib/itsi/server/version.rb +1 -1
  162. data/gems/server/lib/itsi/server.rb +1 -0
  163. data/gems/server/lib/ruby_lsp/itsi/addon.rb +12 -13
  164. data/gems/server/test/helpers/test_helper.rb +12 -13
  165. data/gems/server/test/middleware/grpc/grpc.rb +13 -14
  166. data/gems/server/test/middleware/grpc/test_service_impl.rb +3 -3
  167. data/gems/server/test/middleware/proxy.rb +262 -268
  168. data/lib/itsi/version.rb +1 -1
  169. metadata +97 -6
  170. data/tasks.txt +0 -28
@@ -0,0 +1,19 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class PipelineFlush < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ pipeline_flush ${1|true,false|}
8
+ SNIPPET
9
+
10
+ detail "Aggregates flushes to better support pipelined responses. (HTTP1 only)."
11
+
12
+ schema do
13
+ (Bool() & Required()).default(false)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -2,17 +2,15 @@ module Itsi
2
2
  class Server
3
3
  module Config
4
4
  class ReusePort < Option
5
-
6
5
  insert_text <<~SNIPPET
7
- reuse_port ${1|true,false|}
6
+ reuse_port ${1|true,false|}
8
7
  SNIPPET
9
8
 
10
9
  detail "Configures whether the server should set the reuse_port option on the underlying socket."
11
10
 
12
11
  schema do
13
- (Bool() & Required()).default(false)
12
+ (Bool() & Required()).default(true)
14
13
  end
15
-
16
14
  end
17
15
  end
18
16
  end
@@ -0,0 +1,25 @@
1
+ ---
2
+ title: Write Vectored
3
+ url: /options/writev
4
+ ---
5
+
6
+ Set whether HTTP/1 connections should try to use vectored writes,
7
+ or always flatten into a single buffer.
8
+
9
+ Note that setting this to false may mean more copies of body data,
10
+ but may also improve performance when an IO transport doesn't
11
+ support vectored writes well, such as most TLS implementations.
12
+
13
+ Setting this to true will force hyper to use queued strategy
14
+ which may eliminate unnecessary cloning on some TLS backends
15
+
16
+ Default is `nil` in which case hyper will try to guess which mode to use
17
+
18
+ ## Configuration
19
+ ```ruby {filename=Itsi.rb}
20
+ writev true
21
+ ```
22
+
23
+ ```ruby {filename=Itsi.rb}
24
+ writev false
25
+ ```
@@ -0,0 +1,19 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class Writev < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ writev ${1|true,false|}
8
+ SNIPPET
9
+
10
+ detail "Set whether HTTP/1 connections should try to use vectored writes"
11
+
12
+ schema do
13
+ (Bool() & Required()).default(false)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -44,9 +44,9 @@ module Itsi
44
44
  rate_limit key: "address", store_config: "in_memory", requests: 5, seconds: 10
45
45
  etag type: "strong", algorithm: "md5", min_body_size: 1024 * 1024
46
46
  compress min_size: 1024 * 1024, level: "fastest", algorithms: %w[zstd gzip br deflate],
47
- mime_types: %w[all], compress_streams: true
47
+ mime_types: %w[all], compress_streams: true
48
48
  log_requests before: { level: "DEBUG", format: "[{request_id}] {method} {path_and_query} - {addr} " },
49
- after: { level: "DEBUG",
49
+ after: { level: "DEBUG",
50
50
  format: "[{request_id}] └─ {status} in {response_time}" }
51
51
  nodelay false
52
52
  static_assets \
@@ -65,14 +65,19 @@ module Itsi
65
65
  }
66
66
  end
67
67
  elsif File.exist?(config_file_path.to_s)
68
- DSL.evaluate(config_file_path)
68
+ DSL.evaluate do
69
+ include config_file_path.gsub(".rb", "")
70
+ rackup_file args[:rackup_file], script_name: "/" if args.key?(:rackup_file)
71
+ end
69
72
  elsif File.exist?("./config.ru")
70
73
  DSL.evaluate do
71
74
  preload true
72
- rackup_file args.fetch(:rackup_file, "./config.ru")
75
+ rackup_file args.fetch(:rackup_file, "./config.ru"), script_name: "/"
73
76
  end
74
77
  else
75
- DSL.evaluate {}
78
+ DSL.evaluate do
79
+ rackup_file args[:rackup_file], script_name: "/" if args.key?(:rackup_file)
80
+ end
76
81
  end
77
82
 
78
83
  itsifile_config.transform_keys!(&:to_sym)
@@ -92,7 +97,7 @@ module Itsi
92
97
  errors << [e, e.backtrace[0]]
93
98
  end
94
99
  # If we're just preloading a specific gem group, we'll do that here too
95
- when Symbol
100
+ when Symbol, String
96
101
  Itsi.log_debug("Preloading gem group #{preload}")
97
102
  Bundler.require(preload)
98
103
  end
@@ -106,7 +111,7 @@ module Itsi
106
111
  end
107
112
 
108
113
  srv_config = {
109
- workers: args.fetch(:workers) { itsifile_config.fetch(:workers, 1) },
114
+ workers: args.fetch(:workers) { itsifile_config.fetch(:workers, nil) },
110
115
  worker_memory_limit: args.fetch(:worker_memory_limit) { itsifile_config.fetch(:worker_memory_limit, nil) },
111
116
  silence: args.fetch(:silence) { itsifile_config.fetch(:silence, false) },
112
117
  shutdown_timeout: args.fetch(:shutdown_timeout) { itsifile_config.fetch(:shutdown_timeout, 5) },
@@ -127,7 +132,7 @@ module Itsi
127
132
  multithreaded_reactor: args.fetch(:multithreaded_reactor) do
128
133
  itsifile_config.fetch(:multithreaded_reactor, nil)
129
134
  end,
130
- pin_worker_cores: args.fetch(:pin_worker_cores) { itsifile_config.fetch(:pin_worker_cores, true) },
135
+ pin_worker_cores: args.fetch(:pin_worker_cores) { itsifile_config.fetch(:pin_worker_cores, false) },
131
136
  scheduler_class: args.fetch(:scheduler_class) { itsifile_config.fetch(:scheduler_class, nil) },
132
137
  oob_gc_responses_threshold: args.fetch(:oob_gc_responses_threshold) do
133
138
  itsifile_config.fetch(:oob_gc_responses_threshold, nil)
@@ -139,9 +144,15 @@ module Itsi
139
144
  log_format: args.fetch(:log_format) { itsifile_config.fetch(:log_format, nil) },
140
145
  log_target: args.fetch(:log_target) { itsifile_config.fetch(:log_target, nil) },
141
146
  log_target_filters: args.fetch(:log_target_filters) { itsifile_config.fetch(:log_target_filters, nil) },
147
+ pipeline_flush: itsifile_config.fetch(:pipeline_flush, true),
148
+ writev: itsifile_config.fetch(:writev, false),
149
+ max_concurrent_streams: itsifile_config.fetch(:max_concurrent_streams, nil),
150
+ max_local_error_reset_streams: itsifile_config.fetch(:max_local_error_reset_streams, nil),
151
+ max_header_list_size: itsifile_config.fetch(:max_header_list_size, 2 * 1024 * 1024),
152
+ max_send_buf_size: itsifile_config.fetch(:max_send_buf_size, 64 * 1024),
142
153
  binds: args.fetch(:binds) { itsifile_config.fetch(:binds, ["http://0.0.0.0:3000"]) },
143
154
  middleware_loader: middleware_loader,
144
- listeners: args.fetch(:listeners) { nil },
155
+ listeners: args.fetch(:listeners, nil),
145
156
  reuse_address: itsifile_config.fetch(:reuse_address, true),
146
157
  reuse_port: itsifile_config.fetch(:reuse_port, true),
147
158
  listen_backlog: itsifile_config.fetch(:listen_backlog, 1024),
@@ -222,6 +233,8 @@ module Itsi
222
233
 
223
234
  # Find config file path, if it exists.
224
235
  def self.config_file_path(config_file_path = nil)
236
+ raise "Config file #{config_file_path} does not exist" if config_file_path && !File.exist?(config_file_path)
237
+
225
238
  config_file_path ||= \
226
239
  if File.exist?(ITSI_DEFAULT_CONFIG_FILE)
227
240
  ITSI_DEFAULT_CONFIG_FILE
@@ -10,10 +10,7 @@ env = ENV.fetch("APP_ENV") { ENV.fetch("RACK_ENV", "development") }
10
10
 
11
11
  # Number of worker processes to spawn
12
12
  # If more than 1, Itsi will be booted in Cluster mode
13
- workers ENV.fetch("ITSI_WORKERS") {
14
- require "etc"
15
- env == "development" ? 1 : nil
16
- }
13
+ workers ENV["ITSI_WORKERS"]&.to_i || (env == "development" ? 1 : nil)
17
14
 
18
15
  # Number of threads to spawn per worker process
19
16
  # For pure CPU bound applicationss, you'll get the best results keeping this number low
@@ -30,11 +27,13 @@ threads ENV.fetch("ITSI_THREADS", 3)
30
27
  fiber_scheduler nil
31
28
 
32
29
  # If you bind to https, without specifying a certificate, Itsi will use a self-signed certificate.
33
- # The self-signed certificate will use a CA generated for your host and stored inside `ITSI_LOCAL_CA_DIR` (Defaults to ~/.itsi)
30
+ # The self-signed certificate will use a CA generated for your
31
+ # host and stored inside `ITSI_LOCAL_CA_DIR` (Defaults to ~/.itsi)
34
32
  # bind "https://0.0.0.0:3000"
35
33
  # bind "https://0.0.0.0:3000?domains=dev.itsi.fyi"
36
34
  #
37
- # If you want to use let's encrypt to generate you a real certificate you and pass cert=acme and an acme_email address to generate one.
35
+ # If you want to use let's encrypt to generate you a real certificate you
36
+ # and pass cert=acme and an acme_email address to generate one.
38
37
  # bind "https://itsi.fyi?cert=acme&acme_email=admin@itsi.fyi"
39
38
  # You can generate certificates for multiple domains at once, by passing a comma-separated list of domains
40
39
  # bind "https://0.0.0.0?domains=foo.itsi.fyi,bar.itsi.fyi&cert=acme&acme_email=admin@itsi.fyi"
@@ -71,7 +70,8 @@ preload true
71
70
  # all of them at once, if they reach the threshold simultaneously.
72
71
  worker_memory_limit 1024 * 1024 * 1024
73
72
 
74
- # You can provide an optional block of code to run, when a worker hits its memory threshold (Use this to send yourself an alert,
73
+ # You can provide an optional block of code to run, when a worker hits its memory threshold
74
+ # (Use this to send yourself an alert,
75
75
  # write metrics to disk etc. etc.)
76
76
  after_memory_limit_reached do |pid|
77
77
  puts "Worker #{pid} has reached its memory threshold and will restart"
@@ -88,7 +88,8 @@ after_fork {}
88
88
  shutdown_timeout 5
89
89
 
90
90
  # Set this to false for application environments that require rack.input to be a rewindable body
91
- # (like Rails). For rack applications that can stream inputs, you can set this to true for a more memory-efficient approach.
91
+ # (like Rails). For rack applications that can stream inputs, you can set this to true for a more
92
+ # memory-efficient approach.
92
93
  stream_body false
93
94
 
94
95
  # OOB GC responses threshold
@@ -190,6 +190,8 @@ module Itsi
190
190
  end
191
191
  end
192
192
 
193
+ alias_method :each, :each_remote_read
194
+
193
195
  def bidi_streamer?
194
196
  input_stream? && output_stream?
195
197
  end
@@ -79,7 +79,7 @@ module Itsi
79
79
  end
80
80
 
81
81
  def handle_client_streaming(active_call)
82
- response = service.send(active_call.method_name, active_call.each_remote_read, active_call)
82
+ response = service.send(active_call.method_name, active_call)
83
83
  active_call.remote_send(response)
84
84
  end
85
85
 
@@ -94,7 +94,7 @@ module Itsi
94
94
  end
95
95
 
96
96
  def handle_bidi_streaming(active_call)
97
- result = service.send(active_call.method_name, active_call.each_remote_read, active_call) do |response|
97
+ result = service.send(active_call.method_name, active_call.each, active_call) do |response|
98
98
  active_call.remote_send(response)
99
99
  end
100
100
  return unless result.is_a?(Enumerator)
@@ -8,7 +8,9 @@ module Rack
8
8
  port = options.fetch(:Port, 3001)
9
9
  ::Itsi::Server.start(
10
10
  {
11
- binds: ["http://#{host}:#{port}"]
11
+ binds: ["http://#{host}:#{port}"],
12
+ threads: 5,
13
+ workers: 1
12
14
  }
13
15
  ) do
14
16
  run app
@@ -12,7 +12,7 @@ module Itsi
12
12
  end
13
13
  end
14
14
  lambda do |request|
15
- Server.respond(request, app.call(request.to_rack_env))
15
+ Server.respond(request, app.call(env = request.to_rack_env), env)
16
16
  end
17
17
  end
18
18
 
@@ -20,14 +20,14 @@ module Itsi
20
20
  # Here we build the env, and invoke the Rack app's call method.
21
21
  # We then turn the Rack response into something Itsi server understands.
22
22
  def call(app, request)
23
- respond request, app.call(request.to_rack_env)
23
+ respond request, app.call(env = request.to_rack_env), env
24
24
  end
25
25
 
26
26
  # Itsi responses are asynchronous and can be streamed.
27
27
  # Response chunks are sent using response.send_frame
28
28
  # and the response is finished using response.close_write.
29
29
  # If only a single chunk is written, you can use the #send_and_close method.
30
- def respond(request, (status, headers, body))
30
+ def respond(request, (status, headers, body), env)
31
31
  response = request.response
32
32
 
33
33
  # Don't try and respond if we've been hijacked.
@@ -39,13 +39,16 @@ module Itsi
39
39
 
40
40
  # 2. Set Headers
41
41
  body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack")
42
- headers.each do |key, value|
43
- if value.is_a?(Array)
42
+
43
+ response.reserve_headers(headers.size)
44
+
45
+ for key, value in headers
46
+ case value
47
+ when String then response[key] = value
48
+ when Array
44
49
  value.each do |v|
45
50
  response[key] = v
46
51
  end
47
- elsif value.is_a?(String)
48
- response[key] = value
49
52
  end
50
53
  end
51
54
 
@@ -53,15 +56,15 @@ module Itsi
53
56
  # As soon as we start setting the response
54
57
  # the server will begin to stream it to the client.
55
58
 
56
- # If we're partially hijacked or returned a streaming body,
57
- # stream this response.
58
59
 
59
60
  if body_streamer
61
+ # If we're partially hijacked or returned a streaming body,
62
+ # stream this response.
60
63
  body_streamer.call(response)
61
64
 
62
- # If we're enumerable with more than one chunk
63
- # also stream, otherwise write in a single chunk
64
65
  elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
66
+ # If we're enumerable with more than one chunk
67
+ # also stream, otherwise write in a single chunk
65
68
  unless body.respond_to?(:each)
66
69
  body = body.to_ary
67
70
  raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
@@ -78,8 +81,10 @@ module Itsi
78
81
  else
79
82
  response.send_and_close(body.to_s)
80
83
  end
84
+ rescue EOFError
85
+ response.close
81
86
  ensure
82
- response.close_write
87
+ RackEnvPool.checkin(env)
83
88
  body.close if body.respond_to?(:close)
84
89
  end
85
90
 
@@ -14,6 +14,8 @@ module Itsi
14
14
  def schedule(app, request)
15
15
  Fiber.schedule do
16
16
  app.call(request)
17
+ rescue StandardError => e
18
+ request.server_error(e.message)
17
19
  end
18
20
  end
19
21
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.2.16"
5
+ VERSION = "0.2.18"
6
6
  end
7
7
  end
@@ -12,6 +12,7 @@ require_relative "server/rack/handler/itsi"
12
12
  require_relative "server/config"
13
13
  require_relative "server/typed_handlers"
14
14
  require_relative "standard_headers"
15
+ require_relative "rack_env_pool"
15
16
  require_relative "http_request"
16
17
  require_relative "http_response"
17
18
  require_relative "passfile"
@@ -5,8 +5,8 @@ require "itsi/server"
5
5
 
6
6
  module RubyLsp
7
7
  module Itsi
8
- class Addon < ::RubyLsp::Addon
9
- def activate(global_state, message_queue)
8
+ class Addon < ::RubyLsp::Addon # rubocop:disable Style/Documentation
9
+ def activate(_global_state, message_queue)
10
10
  @message_queue = message_queue
11
11
  end
12
12
 
@@ -20,33 +20,34 @@ module RubyLsp
20
20
  "0.1.0"
21
21
  end
22
22
 
23
-
24
23
  def create_completion_listener(response_builder, node_context, dispatcher, uri)
25
- return unless uri.to_s.end_with?("Itsi.rb")
24
+ return unless uri.to_s =~ /itsi.rb$/i
25
+
26
26
  @in_itsi_file = true
27
27
  CompletionListener.new(response_builder, node_context, dispatcher, uri)
28
28
  end
29
29
 
30
30
  def create_hover_listener(response_builder, node_context, dispatcher)
31
31
  hl = dispatcher.listeners[:on_call_node_enter].find { |c| c.is_a?(RubyLsp::Listeners::Hover) }
32
- return unless hl.instance_variable_get("@path").to_s.end_with?("Itsi.rb")
32
+ return unless hl.instance_variable_get("@path").to_s =~ /itsi.rb$/i
33
+
33
34
  HoverListener.new(response_builder, node_context, dispatcher)
34
35
  end
35
36
  end
36
37
 
37
- class HoverListener
38
+ class HoverListener # rubocop:disable Style/Documentation
38
39
  def initialize(response_builder, node_context, dispatcher)
39
40
  @response_builder = response_builder
40
41
  @node_context = node_context
41
42
  @dispatcher = dispatcher
42
43
 
43
- @options_by_name = ::Itsi::Server::Config::Option.subclasses.group_by(&:option_name).map{|k,v| [k,v.first]}.to_h
44
- @middlewares_by_name = ::Itsi::Server::Config::Middleware.subclasses.group_by(&:middleware_name).map{|k,v| [k,v.first]}.to_h
44
+ @options_by_name = ::Itsi::Server::Config::Option.subclasses.group_by(&:option_name).transform_values(&:first)
45
+ @middlewares_by_name = ::Itsi::Server::Config::Middleware.subclasses.group_by(&:middleware_name).transform_values(&:first)
45
46
  # Register for call nodes for hover events
46
47
  dispatcher.register(self, :on_call_node_enter)
47
48
  end
48
49
 
49
- def on_call_node_enter(node)
50
+ def on_call_node_enter(node) # rubocop:disable Metrics/MethodLength
50
51
  if (matched_class = @options_by_name[node.message.to_sym])
51
52
  @response_builder.push(
52
53
  matched_class.documentation,
@@ -61,7 +62,7 @@ module RubyLsp
61
62
  end
62
63
  end
63
64
 
64
- class CompletionListener
65
+ class CompletionListener # rubocop:disable Style/Documentation
65
66
  def initialize(response_builder, node_context, dispatcher, uri)
66
67
  @response_builder = response_builder
67
68
  @node_context = node_context
@@ -74,7 +75,7 @@ module RubyLsp
74
75
  dispatcher.register(self, :completion_item_resolve)
75
76
  end
76
77
 
77
- def on_call_node_enter(node)
78
+ def on_call_node_enter(node) # rubocop:disable Metrics/AbcSize
78
79
  # Only handle method calls that are being typed (i.e. no arguments yet)
79
80
  return unless node.arguments.nil?
80
81
 
@@ -122,8 +123,6 @@ module RubyLsp
122
123
  end
123
124
  end
124
125
  end
125
-
126
-
127
126
  end
128
127
  end
129
128
  end
@@ -25,17 +25,10 @@ def free_bind(protocol = "http", unix_socket: false)
25
25
  end
26
26
  end
27
27
 
28
- def server(app: nil, app_with_lint: nil, protocol: "http", bind: free_bind(protocol), itsi_rb: nil, cleanup: true,
29
- timeout: 5, &blk)
28
+ def server(
29
+ app: nil, app_with_lint: nil, protocol: "http", bind: free_bind(protocol), itsi_rb: nil, cleanup: true,
30
+ &blk)
30
31
  app ||= Rack::Lint.new(app_with_lint) if app_with_lint
31
- itsi_rb ||= lambda do
32
- # Inline Itsi.rb
33
- bind bind
34
- workers 1
35
- threads 1
36
- log_level :warn
37
- run app if app
38
- end
39
32
 
40
33
  cli_params = {}
41
34
  cli_params[:binds] = [bind] if bind
@@ -46,13 +39,18 @@ def server(app: nil, app_with_lint: nil, protocol: "http", bind: free_bind(proto
46
39
  sync.push(true)
47
40
  end
48
41
 
49
- Itsi::Server.start_in_background_thread(cli_params, &itsi_rb)
42
+ Itsi::Server.start_in_background_thread(cli_params) do
43
+ bind bind
44
+ workers 1
45
+ threads 1
46
+ log_level :warn
47
+ run app if app
48
+ instance_exec(&itsi_rb) if itsi_rb
49
+ end
50
50
 
51
51
  sync.pop
52
52
  uri = URI(bind)
53
- # Timeout.timeout(timeout) do
54
53
  RequestContext.new(uri, self).instance_exec(uri, &blk)
55
- # end
56
54
  rescue StandardError => e
57
55
  puts e
58
56
  # puts e.message
@@ -119,6 +117,7 @@ class RequestContext
119
117
  def patch(path, data = "", headers = {})
120
118
  request = Net::HTTP::Patch.new(uri_for(path))
121
119
  request.body = data
120
+ headers.each { |k, v| request[k] = v }
122
121
  client.request(request)
123
122
  end
124
123
 
@@ -5,14 +5,13 @@ class TestGrpc < Minitest::Test
5
5
  Stub = Test::TestService::Stub
6
6
 
7
7
  def new_stub(uri)
8
- address = uri.to_s[/\/\/(.*)/,1] # e.g. "127.0.0.1:12345"
8
+ address = uri.to_s[%r{//(.*)}, 1] # e.g. "127.0.0.1:12345"
9
9
  channel = GRPC::Core::Channel.new(address,
10
- {}, # channel_args hash
11
- :this_channel_is_insecure,
12
- )
10
+ {}, # channel_args hash
11
+ :this_channel_is_insecure)
13
12
  stub = Stub.new(nil,
14
- nil,
15
- channel_override: channel)
13
+ nil,
14
+ channel_override: channel)
16
15
  end
17
16
 
18
17
  def test_unary_echo
@@ -46,7 +45,7 @@ class TestGrpc < Minitest::Test
46
45
  req = Test::EchoRequest.new(message: "xy")
47
46
  # expect two StreamResponse frames, one per character
48
47
  chars = stub.server_stream(req).map(&:messages).flatten
49
- assert_equal ["x", "y"], chars
48
+ assert_equal %w[x y], chars
50
49
  end
51
50
  end
52
51
 
@@ -58,7 +57,7 @@ class TestGrpc < Minitest::Test
58
57
  inputs = %w[test1 test2].map { |m| Test::EchoRequest.new(message: m) }
59
58
  # each response uppercases its incoming message
60
59
  results = stub.bidi_stream(inputs.each).map(&:message)
61
- assert_equal ["TEST1", "TEST2"], results
60
+ assert_equal %w[TEST1 TEST2], results
62
61
  end
63
62
  end
64
63
 
@@ -82,7 +81,6 @@ class TestGrpc < Minitest::Test
82
81
  end
83
82
  end
84
83
 
85
-
86
84
  def test_unary_echo_json
87
85
  server(itsi_rb: lambda do
88
86
  grpc TestServiceImpl.new
@@ -99,7 +97,7 @@ class TestGrpc < Minitest::Test
99
97
  server(itsi_rb: lambda do
100
98
  grpc TestServiceImpl.new
101
99
  end) do
102
- input = [{"message" => "a"}, {"message" => "b"}, {"message" => "c"}]
100
+ input = [{ "message" => "a" }, { "message" => "b" }, { "message" => "c" }]
103
101
  res = post("test.TestService/ClientStream", input.to_json, { "Content-Type" => "application/json" })
104
102
  json = JSON.parse(res.body)
105
103
  assert_equal 200, res.code.to_i
@@ -117,7 +115,7 @@ class TestGrpc < Minitest::Test
117
115
  # service streams back one JSON object per character
118
116
  messages = arr.flat_map { |frame| frame["messages"] }
119
117
  assert_equal 200, res.code.to_i
120
- assert_equal ["x","y"], messages
118
+ assert_equal %w[x y], messages
121
119
  end
122
120
  end
123
121
 
@@ -125,13 +123,13 @@ class TestGrpc < Minitest::Test
125
123
  server(itsi_rb: lambda do
126
124
  grpc TestServiceImpl.new
127
125
  end) do
128
- inputs = [{"message":"foo"}, {"message":"Bar"}]
126
+ inputs = [{ "message": "foo" }, { "message": "Bar" }]
129
127
  res = post("test.TestService/BidiStream", inputs.to_json, { "Content-Type" => "application/json" })
130
128
  arr = JSON.parse(res.body)
131
129
  # bidi uppercases each incoming message
132
130
  results = arr.map { |frame| frame["message"] }
133
131
  assert_equal 200, res.code.to_i
134
- assert_equal ["FOO","BAR"], results
132
+ assert_equal %w[FOO BAR], results
135
133
  end
136
134
  end
137
135
 
@@ -145,7 +143,8 @@ class TestGrpc < Minitest::Test
145
143
  assert_nil json1["messages"]
146
144
 
147
145
  # empty server-stream
148
- res2 = post("test.TestService/ServerStream", { "message" => "" }.to_json, { "Content-Type" => "application/json" })
146
+ res2 = post("test.TestService/ServerStream", { "message" => "" }.to_json,
147
+ { "Content-Type" => "application/json" })
149
148
  arr2 = JSON.parse(res2.body)
150
149
  assert arr2.empty?
151
150
 
@@ -1,12 +1,12 @@
1
- require_relative 'test_service_services_pb' # generated by grpc_tools_ruby_protoc
1
+ require_relative "test_service_services_pb" # generated by grpc_tools_ruby_protoc
2
2
 
3
3
  class TestServiceImpl < Test::TestService::Service
4
4
  def unary_echo(req, _unused_call)
5
5
  Test::EchoResponse.new(message: req.message)
6
6
  end
7
7
 
8
- def client_stream(stream, _call)
9
- msgs = stream.map(&:message)
8
+ def client_stream(call)
9
+ msgs = call.each_remote_read.map(&:message)
10
10
  Test::StreamResponse.new(messages: msgs)
11
11
  end
12
12