patient_http-solid_queue 1.0.0

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.
data/README.md ADDED
@@ -0,0 +1,598 @@
1
+ # PatientHttp::SolidQueue
2
+
3
+ [![Continuous Integration](https://github.com/bdurand/patient_http-solid_queue/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/patient_http-solid_queue/actions/workflows/continuous_integration.yml)
4
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
+ [![Gem Version](https://badge.fury.io/rb/patient_http-solid_queue.svg)](https://badge.fury.io/rb/patient_http-solid_queue)
6
+
7
+ *Built for APIs that like to think.*
8
+
9
+ This gem provides a mechanism to offload HTTP requests to a dedicated async I/O processor running in your Solid Queue worker process using the [patient_http gem](https://github.com/bdurand/patient_http). Worker threads are freed immediately while HTTP requests are in flight so that they can do other work instead of waiting for HTTP responses.
10
+
11
+ ## Motivation
12
+
13
+ Solid Queue is designed with the assumption that jobs are short-lived and complete quickly. Long-running HTTP requests block worker threads from processing other jobs, leading to increased latency and reduced throughput. This is particularly problematic when calling LLM or AI APIs, where requests can take many seconds to complete.
14
+
15
+ **The Problem:**
16
+
17
+ ```
18
+ ┌────────────────────────────────────────────────────────────────────────┐
19
+ │ Traditional Solid Queue Job │
20
+ │ │
21
+ │ Worker Thread 1: [████████████ HTTP Request (5s) ████████████████] │
22
+ │ Worker Thread 2: [████████████ HTTP Request (5s) ████████████████] │
23
+ │ Worker Thread 3: [████████████ HTTP Request (5s) ████████████████] │
24
+ │ │
25
+ │ → 3 workers blocked for 5 seconds = 0 jobs processed │
26
+ └────────────────────────────────────────────────────────────────────────┘
27
+ ```
28
+
29
+ **The Solution:**
30
+
31
+ ```
32
+ ┌────────────────────────────────────────────────────────────────────────┐
33
+ │ With Async HTTP Processor │
34
+ │ │
35
+ │ Worker Thread 1: [█ Enqueue █][█ Job █][█ Job █][█ Job █][█ Job █] │
36
+ │ Worker Thread 2: [█ Enqueue █][█ Job █][█ Job █][█ Job █][█ Job █] │
37
+ │ Worker Thread 3: [█ Enqueue █][█ Job █][█ Job █][█ Job █][█ Job █] │
38
+ │ │
39
+ │ Async Processor: [═══════════ 100+ concurrent HTTP requests ════════] │
40
+ │ │
41
+ │ → Workers immediately free = dozens of jobs processed │
42
+ └────────────────────────────────────────────────────────────────────────┘
43
+ ```
44
+
45
+ The async processor runs in a dedicated thread within your Solid Queue worker process, using Ruby's Fiber-based concurrency to handle hundreds of concurrent HTTP requests without blocking. When an HTTP request completes, a callback service is invoked for processing.
46
+
47
+ ## Quick Start
48
+
49
+ ### 1. Create a Callback Service
50
+
51
+ Define a callback service class with `on_complete` and `on_error` methods:
52
+
53
+ ```ruby
54
+ class FetchDataCallback
55
+ def on_complete(response)
56
+ user_id = response.callback_args[:user_id]
57
+ if response.success?
58
+ data = response.json
59
+ User.find(user_id).update!(external_data: data)
60
+ else
61
+ Rails.logger.error("HTTP #{response.status} fetching data for user #{user_id}")
62
+ end
63
+ end
64
+
65
+ def on_error(error)
66
+ user_id = error.callback_args[:user_id]
67
+ Rails.logger.error("Failed to fetch data for user #{user_id}: #{error.message}")
68
+ end
69
+ end
70
+ ```
71
+
72
+ ### 2. Make HTTP Requests
73
+
74
+ Make HTTP requests from anywhere in your code using `PatientHttp`:
75
+
76
+ ```ruby
77
+ PatientHttp.get(
78
+ "https://api.example.com/users/#{user_id}",
79
+ headers: {"Authorization" => "Bearer #{ENV['API_KEY']}"},
80
+ callback: FetchDataCallback,
81
+ callback_args: {user_id: user_id}
82
+ )
83
+ ```
84
+
85
+ ### 3. That's It!
86
+
87
+ The request will be enqueued as an Active Job and passed to a [PatientHttp](https://github.com/bdurand/patient_http) processor to execute asynchronously. When the HTTP request completes, your callback's `on_complete` method is executed in another Active Job.
88
+
89
+ If an error occurs during the request, the `on_error` method is called instead.
90
+
91
+ You can also call `PatientHttp.post`, `PatientHttp.put`, `PatientHttp.patch`, and `PatientHttp.delete` for other HTTP methods. See the [patient_http docs](https://github.com/bdurand/patient_http) for the full API reference.
92
+
93
+ The `response.callback_args` and `error.callback_args` provide access to the arguments you passed via the `callback_args` option.
94
+
95
+ > [!IMPORTANT]
96
+ > Do not re-raise errors in the `on_error` callback as a means to retry the request. That will just retry the error callback job. If you want to retry the original request, you can enqueue a new request from within `on_error`. Be careful with this approach, though, as it can lead to infinite retry loops if the error condition is not resolved.
97
+ >
98
+ > Also note that the error callback is only called when an exception occurs during the HTTP request (timeout, connection failure, etc). HTTP error status codes (4xx, 5xx) do not trigger the error callback by default. Instead, they are treated as completed requests and passed to the `on_complete` callback. See the "Handling HTTP Error Responses" section below for how to treat HTTP errors as exceptions.
99
+
100
+ ### Handling HTTP Error Responses
101
+
102
+ By default, HTTP error status codes (4xx, 5xx) are treated as successful responses and passed to the `on_complete` callback. You can check the status using `response.success?`, `response.client_error?`, or `response.server_error?`:
103
+
104
+ ```ruby
105
+ class ApiCallback
106
+ def on_complete(response)
107
+ if response.success?
108
+ process_data(response.json)
109
+ elsif response.client_error?
110
+ handle_client_error(response.status, response.body)
111
+ elsif response.server_error?
112
+ handle_server_error(response.status, response.body)
113
+ end
114
+ end
115
+
116
+ def on_error(error)
117
+ Rails.logger.error("Request failed: #{error.message}")
118
+ end
119
+ end
120
+
121
+ PatientHttp.get(
122
+ "https://api.example.com/data/#{id}",
123
+ callback: ApiCallback
124
+ )
125
+ ```
126
+
127
+ If you prefer to treat HTTP errors as exceptions, you can use the `raise_error_responses` option. When enabled, non-2xx responses will call the `on_error` callback with an `HttpError` instead:
128
+
129
+ ```ruby
130
+ class ApiCallback
131
+ def on_complete(response)
132
+ # Only called for 2xx responses
133
+ process_data(response.json)
134
+ end
135
+
136
+ def on_error(error)
137
+ # Called for exceptions AND HTTP errors when using raise_error_responses
138
+ if error.is_a?(PatientHttp::HttpError)
139
+ # Access the response via error.response
140
+ Rails.logger.error("HTTP #{error.status} from #{error.url}: #{error.response.body}")
141
+ else
142
+ # Regular request errors (timeout, connection, etc)
143
+ Rails.logger.error("Request failed: #{error.message}")
144
+ end
145
+ end
146
+ end
147
+
148
+ PatientHttp.get(
149
+ "https://api.example.com/data/#{id}",
150
+ callback: ApiCallback,
151
+ raise_error_responses: true
152
+ )
153
+ ```
154
+
155
+ The `HttpError` provides convenient access to the response:
156
+
157
+ ```ruby
158
+ def on_error(error)
159
+ if error.is_a?(PatientHttp::HttpError)
160
+ puts error.status # HTTP status code
161
+ puts error.url # Request URL
162
+ puts error.http_method # HTTP method
163
+ puts error.response.body # Response body
164
+ puts error.response.headers # Response headers
165
+ puts error.response.json # Parse JSON response (if applicable)
166
+ end
167
+ end
168
+ ```
169
+
170
+ ## Usage Patterns
171
+
172
+ ### Making Requests
173
+
174
+ The primary interface for making requests is through the `PatientHttp` module, which provides convenience methods for all HTTP verbs:
175
+
176
+ ```ruby
177
+ # GET request
178
+ PatientHttp.get("https://api.example.com/users/123",
179
+ callback: MyCallback, callback_args: {user_id: 123})
180
+
181
+ # POST request with JSON body
182
+ PatientHttp.post("https://api.example.com/users",
183
+ json: {name: "John", email: "john@example.com"},
184
+ callback: MyCallback)
185
+
186
+ # PUT request
187
+ PatientHttp.put("https://api.example.com/users/123",
188
+ json: {name: "Updated Name"},
189
+ callback: MyCallback)
190
+
191
+ # PATCH request
192
+ PatientHttp.patch("https://api.example.com/users/123",
193
+ json: {status: "active"},
194
+ callback: MyCallback)
195
+
196
+ # DELETE request
197
+ PatientHttp.delete("https://api.example.com/users/123",
198
+ callback: MyCallback)
199
+ ```
200
+
201
+ Available request options:
202
+
203
+ - `callback:` - (required) Callback service class or class name
204
+ - `callback_args:` - Hash of arguments passed to callback via response/error
205
+ - `headers:` - Request headers
206
+ - `body:` - Request body (for POST/PUT/PATCH)
207
+ - `json:` - Object to serialize as JSON body (cannot use with body)
208
+ - `params:` - Query parameters to append to URL
209
+ - `timeout:` - Request timeout in seconds
210
+ - `raise_error_responses:` - Treat non-2xx responses as errors
211
+
212
+ You can also build a `PatientHttp::Request` object and pass it to `PatientHttp.execute` for more control:
213
+
214
+ ```ruby
215
+ request = PatientHttp::Request.new(:get, "https://api.example.com/users/123",
216
+ headers: {"Authorization" => "Bearer token"},
217
+ params: {include: "profile"},
218
+ timeout: 30
219
+ )
220
+ PatientHttp.execute(request: request, callback: MyCallback, callback_args: {user_id: 123})
221
+ ```
222
+
223
+ See the [patient_http docs](https://github.com/bdurand/patient_http) for the full `Request` and `Response` API reference.
224
+
225
+ ### Using Request Templates
226
+
227
+ For repeated requests to the same API, use `PatientHttp::RequestTemplate` to share configuration:
228
+
229
+ ```ruby
230
+ class ApiService
231
+ def initialize
232
+ @template = PatientHttp::RequestTemplate.new(
233
+ base_url: "https://api.example.com",
234
+ headers: {"Authorization" => "Bearer #{ENV['API_KEY']}"},
235
+ timeout: 60
236
+ )
237
+ end
238
+
239
+ def fetch_user(user_id)
240
+ request = @template.get("/users/#{user_id}")
241
+ PatientHttp.execute(
242
+ request: request,
243
+ callback: FetchUserCallback,
244
+ callback_args: {user_id: user_id}
245
+ )
246
+ end
247
+
248
+ def update_user(user_id, attributes)
249
+ request = @template.patch("/users/#{user_id}", json: attributes)
250
+ PatientHttp.execute(
251
+ request: request,
252
+ callback: UpdateUserCallback,
253
+ callback_args: {user_id: user_id}
254
+ )
255
+ end
256
+ end
257
+ ```
258
+
259
+ ### Using the RequestHelper Module
260
+
261
+ For classes that make many async HTTP requests, you can include `PatientHttp::RequestHelper` to get convenient instance methods like `async_get`, `async_post`, `async_put`, `async_patch`, and `async_delete`. You can also define a request template at the class level using the `request_template` class method to set shared options like `base_url`, `headers`, and `timeout`.
262
+
263
+ When using this gem, the request handler is automatically registered when the processor starts and unregistered when it stops — no manual setup is required.
264
+
265
+ ```ruby
266
+ class NotificationService
267
+ include PatientHttp::RequestHelper
268
+
269
+ request_template base_url: "https://api.example.com",
270
+ headers: {"Authorization" => "Bearer #{ENV['API_KEY']}"},
271
+ timeout: 30
272
+
273
+ def notify_user(user_id, message)
274
+ async_post("/notifications",
275
+ json: {user_id: user_id, message: message},
276
+ callback: NotificationCallback,
277
+ callback_args: {user_id: user_id}
278
+ )
279
+ end
280
+
281
+ def fetch_user(user_id)
282
+ async_get("/users/#{user_id}",
283
+ callback: FetchUserCallback,
284
+ callback_args: {user_id: user_id}
285
+ )
286
+ end
287
+ end
288
+ ```
289
+
290
+ The `async_*` methods accept the same options as `PatientHttp.get`, `PatientHttp.post`, etc. Paths are resolved relative to the `base_url` defined in the request template.
291
+
292
+ See the [patient_http gem](https://github.com/bdurand/patient_http) for the full `RequestHelper` documentation.
293
+
294
+ ### Callback Arguments
295
+
296
+ Pass custom data to your callbacks using the `callback_args` option:
297
+
298
+ ```ruby
299
+ class FetchDataCallback
300
+ def on_complete(response)
301
+ # Access callback_args using symbol or string keys
302
+ user_id = response.callback_args[:user_id]
303
+ request_timestamp = response.callback_args[:request_timestamp]
304
+
305
+ User.find(user_id).update!(
306
+ external_data: response.json,
307
+ fetched_at: request_timestamp
308
+ )
309
+ end
310
+
311
+ def on_error(error)
312
+ user_id = error.callback_args[:user_id]
313
+ request_timestamp = error.callback_args[:request_timestamp]
314
+
315
+ Rails.logger.error(
316
+ "Failed to fetch data for user #{user_id} at #{request_timestamp}: #{error.message}"
317
+ )
318
+ end
319
+ end
320
+
321
+ # Pass data via callback_args option
322
+ PatientHttp.get(
323
+ "https://api.example.com/users/#{user_id}",
324
+ callback: FetchDataCallback,
325
+ callback_args: {
326
+ user_id: user_id,
327
+ request_timestamp: Time.now.iso8601
328
+ }
329
+ )
330
+ ```
331
+
332
+ **Important details about callback_args:**
333
+
334
+ - Must be a Hash (or respond to `to_h`) containing only JSON-native types: `nil`, `true`, `false`, `String`, `Integer`, `Float`, `Array`, or `Hash`
335
+ - Hash keys will be converted to strings for serialization
336
+ - Nested hashes and hashes in arrays also have their keys converted to strings
337
+ - You can access callback_args using either symbol or string keys: `callback_args[:user_id]` or `callback_args["user_id"]`
338
+
339
+ ### Sensitive Data Handling
340
+
341
+ Requests and responses from asynchronous HTTP requests may be stored in your queue backend (and optionally external storage) in order to execute completion callbacks. This can raise security concerns if they contain sensitive data since the data will be stored in plain text.
342
+
343
+ Encryption is configured on the parent `patient_http` gem. You can set an `encryption_key` to automatically encrypt and decrypt request and response data using `ActiveSupport::MessageEncryptor`:
344
+
345
+ ```ruby
346
+ PatientHttp::SolidQueue.configure do |config|
347
+ config.encryption_key = Rails.application.credentials.patient_http_secret
348
+ end
349
+ ```
350
+
351
+ See the [patient_http gem](https://github.com/bdurand/patient_http) for full documentation on encryption options, including key rotation and custom encryption callables.
352
+
353
+ ## Configuration
354
+
355
+ The gem can be configured globally in an initializer:
356
+
357
+ ```ruby
358
+ PatientHttp::SolidQueue.configure do |config|
359
+ # Maximum concurrent HTTP requests (default: 256)
360
+ config.max_connections = 256
361
+
362
+ # Default timeout for HTTP requests in seconds (default: 60)
363
+ config.request_timeout = 60
364
+
365
+ # Maximum number of host clients to pool (default: 100)
366
+ config.connection_pool_size = 100
367
+
368
+ # Connection timeout in seconds (default: nil, uses request_timeout)
369
+ config.connection_timeout = 10
370
+
371
+ # Number of retries for failed requests (default: 3)
372
+ config.retries = 3
373
+
374
+ # Handler called when a callback job exhausts all Sidekiq retries
375
+ config.on_retries_exhausted { |error| MyAlertService.notify(error) }
376
+
377
+ # HTTP/HTTPS proxy URL (default: nil)
378
+ # Supports authentication: "http://user:pass@proxy.example.com:8080"
379
+ config.proxy_url = "http://proxy.example.com:8080"
380
+
381
+ # Default User-Agent header for all requests (default: "PatientHttp")
382
+ config.user_agent = "MyApp/1.0"
383
+
384
+ # Timeout for graceful shutdown in seconds
385
+ # (default: SolidQueue.shutdown_timeout - 2)
386
+ # This should be less than your worker shutdown timeout
387
+ config.shutdown_timeout = 23
388
+
389
+ # Maximum response body size in bytes (default: 1MB)
390
+ # Responses larger than this will trigger ResponseTooLargeError
391
+ config.max_response_size = 1024 * 1024
392
+
393
+ # Maximum number of redirects to follow (default: 5, 0 disables)
394
+ config.max_redirects = 5
395
+
396
+ # Whether to raise HttpError for non-2xx responses by default (default: false)
397
+ config.raise_error_responses = false
398
+
399
+ # Heartbeat interval for crash recovery in seconds (default: 60)
400
+ config.heartbeat_interval = 60
401
+
402
+ # Orphan detection threshold in seconds (default: 300)
403
+ # Requests older than this without a heartbeat will be re-enqueued
404
+ config.orphan_threshold = 300
405
+
406
+ # Size threshold in bytes for external payload storage (default: 64KB)
407
+ # Payloads larger than this will be stored externally when a payload
408
+ # store is configured.
409
+ config.payload_store_threshold = 64 * 1024
410
+
411
+ # Queue name for RequestJob and CallbackJob (default: nil, Active Job default)
412
+ config.queue_name = "async_http"
413
+
414
+ # Custom logger (defaults to SolidQueue.logger)
415
+ config.logger = Rails.logger
416
+
417
+ # Encryption key for sensitive data (see Sensitive Data Handling)
418
+ # Accepts a string or an array of strings for key rotation.
419
+ # Encryption is provided by the parent patient_http gem.
420
+ config.encryption_key = Rails.application.credentials.patient_http_secret
421
+ end
422
+ ```
423
+
424
+ See the [Configuration](lib/patient_http/solid_queue/configuration.rb) class for all available options.
425
+
426
+ ### Tuning Tips
427
+
428
+ - `max_connections`: Adjust this based on your system's resources. Each connection uses memory and file descriptors. A tuned system with sufficient resources can handle thousands of concurrent connections.
429
+ - `request_timeout`: Set this based on the expected response times of the APIs you are calling. AI APIs might sometimes take minutes to respond as they generate content.
430
+ - `connection_pool_size`: Controls how many connections to different hosts are kept alive. Increase for applications calling many different API endpoints.
431
+ - `connection_timeout`: Set this if you need to fail fast on connection establishment. Useful for detecting network issues quickly.
432
+ - `retries`: Number of times to retry a failed request before calling the error callback.
433
+ - `max_response_size`: Set this to limit the maximum size of HTTP responses. This helps prevent excessive memory usage from unexpectedly large responses. Responses need to be serialized as Active Job arguments and very large responses may cause performance issues. If a response body is text content, it will be compressed to save space. However, binary content needs to be Base64 encoded which increases size by ~33%.
434
+ - `payload_store_threshold`: Lower this if your queue backend struggles with large payloads; higher values avoid extra external storage reads/writes.
435
+ - `heartbeat_interval` and `orphan_threshold`: For high-churn workloads, keep `heartbeat_interval` as large as your recovery SLO allows (while still less than `orphan_threshold`) to reduce write/update pressure on monitoring tables. If Solid Queue uses PostgreSQL and request volume is high, tune autovacuum for the queue database tables because `inflight_requests` is intentionally insert/update/delete heavy.
436
+
437
+ > [!IMPORTANT]
438
+ >
439
+ > One difference between using this gem and making synchronous HTTP requests from a Solid Queue job is that if `max_connections` is reached due to slow asynchronous requests, new requests will trigger an error on the Active Job. The Active Job retry mechanism will handle re-enqueuing the job.
440
+ >
441
+ > In contrast, slow synchronous HTTP requests will fill up the worker pool and block new jobs from being dequeued until a worker thread becomes free.
442
+ >
443
+ > In general, the former behavior is preferable because it allows Solid Queue to continue processing other jobs and prevents getting into a state with 1000's of jobs stuck in the queue.
444
+
445
+ ## Metrics and Monitoring
446
+
447
+ ### Callbacks for Custom Monitoring
448
+
449
+ You can register callbacks to integrate with your monitoring system using the `after_completion` and `after_error` hooks:
450
+
451
+ ```ruby
452
+ PatientHttp::SolidQueue.after_completion do |response|
453
+ StatsD.timing("patient_http.duration", response.duration * 1000)
454
+ StatsD.increment("patient_http.status.#{response.status}")
455
+ end
456
+
457
+ PatientHttp::SolidQueue.after_error do |error|
458
+ error_type = error.is_a?(PatientHttp::Error) ? error.error_type : "exception"
459
+ StatsD.increment("patient_http.error.#{error_type}")
460
+ Rails.logger.error("Async HTTP error: #{error.class.name} - #{error.message}")
461
+ end
462
+ ```
463
+
464
+ You can register multiple callbacks; they will be called in the order registered.
465
+
466
+ ## Shutdown Behavior
467
+
468
+ The async HTTP processor automatically hooks in with Solid Queue's lifecycle events.
469
+
470
+ 1. **Startup:** Processor starts automatically when Solid Queue starts a worker
471
+ 2. **Shutdown:** Processor waits up to `shutdown_timeout` seconds for in-flight requests to complete
472
+
473
+ ### Incomplete Request Handling
474
+
475
+ If requests are still in-flight when shutdown times out:
476
+
477
+ - In-flight requests are interrupted
478
+ - The **original Active Job** is automatically re-enqueued
479
+ - Re-enqueued jobs will be processed again when workers are available
480
+
481
+ This ensures no work is lost during deployments or restarts.
482
+
483
+ ### Crash Recovery
484
+
485
+ The gem includes crash recovery to handle process failures:
486
+
487
+ 1. **Heartbeat Tracking:** Every `heartbeat_interval` seconds, the processor updates heartbeat timestamps for all in-flight requests in the database
488
+ 2. **Orphan Detection:** One processor periodically checks for requests that haven't received a heartbeat update in `orphan_threshold` seconds
489
+ 3. **Automatic Re-enqueue:** Orphaned requests have their original Active Jobs re-enqueued
490
+
491
+ This ensures that if a worker process crashes, its in-flight requests will be retried by another process.
492
+
493
+ ## Testing
494
+
495
+ The gem supports testing with Active Job test adapters. When in test mode (`PatientHttp.testing?`), async HTTP requests are executed immediately within the worker thread, blocking until completion. This allows you to write tests that verify the full request/response cycle without needing the async processor to be running.
496
+
497
+ ## Installation
498
+
499
+ Add this line to your application's Gemfile:
500
+
501
+ ```ruby
502
+ gem "patient_http-solid_queue"
503
+ ```
504
+
505
+ Then execute:
506
+
507
+ ```bash
508
+ bundle install
509
+ ```
510
+
511
+ Install and run the gem migrations:
512
+
513
+ ```bash
514
+ bin/rails patient_http_solid_queue:install:migrations
515
+ bin/rails db:migrate
516
+ ```
517
+
518
+ The database tables are used for crash recovery and monitoring of in-flight requests and need to be added to the same database that Solid Queue uses.
519
+
520
+ By default, this install task copies migrations to the `queue` database migration path (typically `db/queue_migrate`).
521
+ If your Solid Queue database name is different, override it with `DATABASE=your_database_name`.
522
+
523
+ For a typical multi-database setup, ensure your `queue` database config defines its own migration path:
524
+
525
+ ```yaml
526
+ development:
527
+ primary:
528
+ adapter: sqlite3
529
+ database: storage/development.sqlite3
530
+ queue:
531
+ adapter: sqlite3
532
+ database: storage/development_queue.sqlite3
533
+ migrations_paths:
534
+ - db/queue_migrate
535
+ ```
536
+
537
+ PostgreSQL example:
538
+
539
+ ```yaml
540
+ development:
541
+ primary:
542
+ adapter: postgresql
543
+ database: my_app_development
544
+ queue:
545
+ adapter: postgresql
546
+ database: my_app_queue_development
547
+ migrations_paths:
548
+ - db/queue_migrate
549
+ ```
550
+
551
+ Then run:
552
+
553
+ ```bash
554
+ bin/rails patient_http_solid_queue:install:migrations
555
+ bin/rails db:queue:migrate
556
+ ```
557
+
558
+ If your Solid Queue database is not named `queue`, pass its name explicitly when installing migrations:
559
+
560
+ ```bash
561
+ bin/rails patient_http_solid_queue:install:migrations DATABASE=solid_queue
562
+ bin/rails db:migrate:solid_queue
563
+ ```
564
+
565
+ ## Contributing
566
+
567
+ Open a pull request on [GitHub](https://github.com/bdurand/patient_http-solid_queue).
568
+
569
+ Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
570
+
571
+ Run the test suite with:
572
+
573
+ ```bash
574
+ bundle exec rake
575
+ ```
576
+
577
+ There is also a bundled test app in the `test_app` directory that can be used for manual testing and experimentation.
578
+
579
+ To run the test app, first install the dependencies:
580
+
581
+ ```bash
582
+ bundle exec rake test_app:bundle
583
+ ```
584
+
585
+ The server will run on http://localhost:9292 and can be started with:
586
+
587
+ ```bash
588
+ bundle exec rake test_app
589
+ ```
590
+
591
+ ## Further Reading
592
+
593
+ - [Architecture](ARCHITECTURE.md)
594
+
595
+
596
+ ## License
597
+
598
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreatePatientHttpSolidQueueTables < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :patient_http_solid_queue_inflight_requests do |t|
6
+ t.string :task_id, null: false, index: {unique: true}
7
+ t.string :process_id, null: false, index: true
8
+ t.text :job_payload, null: false
9
+ t.datetime :heartbeat_at, precision: 6, null: false, index: true
10
+ t.datetime :created_at, precision: 6, null: false
11
+ end
12
+
13
+ create_table :patient_http_solid_queue_processes do |t|
14
+ t.string :process_id, null: false, index: {unique: true}
15
+ t.integer :max_connections, null: false
16
+ t.datetime :last_seen_at, precision: 6, null: false
17
+ end
18
+
19
+ create_table :patient_http_solid_queue_gc_locks do |t|
20
+ t.string :lock_name, null: false, index: {unique: true}
21
+ t.string :lock_holder
22
+ t.datetime :acquired_at, precision: 6
23
+ t.datetime :expires_at, precision: 6
24
+ t.datetime :last_gc_at, precision: 6
25
+ end
26
+ end
27
+ end