patient_http 1.6.1 → 1.7.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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +8 -7
- data/CHANGELOG.md +29 -0
- data/README.md +539 -515
- data/VERSION +1 -1
- data/lib/patient_http/callback_args.rb +53 -48
- data/lib/patient_http/callback_validator.rb +11 -7
- data/lib/patient_http/class_helper.rb +6 -7
- data/lib/patient_http/client.rb +28 -22
- data/lib/patient_http/client_pool.rb +134 -39
- data/lib/patient_http/completion_executor.rb +20 -20
- data/lib/patient_http/configuration.rb +367 -119
- data/lib/patient_http/connection_endpoint.rb +150 -0
- data/lib/patient_http/encryptor.rb +28 -18
- data/lib/patient_http/error.rb +24 -18
- data/lib/patient_http/external_storage.rb +42 -38
- data/lib/patient_http/http_error.rb +30 -26
- data/lib/patient_http/http_headers.rb +35 -29
- data/lib/patient_http/immediate_retries.rb +98 -0
- data/lib/patient_http/inline_task_handler.rb +15 -10
- data/lib/patient_http/lifecycle_manager.rb +39 -40
- data/lib/patient_http/outgoing_request.rb +25 -23
- data/lib/patient_http/payload.rb +28 -26
- data/lib/patient_http/payload_store/active_record_store.rb +31 -34
- data/lib/patient_http/payload_store/base.rb +42 -46
- data/lib/patient_http/payload_store/file_store.rb +22 -26
- data/lib/patient_http/payload_store/redis_store.rb +28 -34
- data/lib/patient_http/payload_store/s3_store.rb +25 -28
- data/lib/patient_http/payload_store.rb +2 -0
- data/lib/patient_http/processor.rb +111 -79
- data/lib/patient_http/processor_observer.rb +65 -59
- data/lib/patient_http/rails/engine.rb +13 -8
- data/lib/patient_http/redirect_error.rb +50 -41
- data/lib/patient_http/redirect_helper.rb +38 -38
- data/lib/patient_http/request.rb +70 -46
- data/lib/patient_http/request_error.rb +47 -42
- data/lib/patient_http/request_helper.rb +142 -119
- data/lib/patient_http/request_preparer.rb +13 -10
- data/lib/patient_http/request_task.rb +113 -84
- data/lib/patient_http/request_template.rb +87 -64
- data/lib/patient_http/response.rb +58 -52
- data/lib/patient_http/response_reader.rb +66 -65
- data/lib/patient_http/secret_manager.rb +34 -30
- data/lib/patient_http/secret_reference.rb +33 -26
- data/lib/patient_http/synchronous_executor.rb +67 -95
- data/lib/patient_http/task_handler.rb +23 -19
- data/lib/patient_http/time_helper.rb +8 -8
- data/lib/patient_http.rb +311 -186
- data/patient_http.gemspec +3 -2
- metadata +21 -5
data/lib/patient_http/payload.rb
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# Encodes request and response bodies so they can be serialized to JSON.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Large text bodies are compressed with gzip, and binary bodies are encoded
|
|
7
|
+
# with Base64.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
8
10
|
class Payload
|
|
9
|
-
# @return [Symbol]
|
|
11
|
+
# @return [Symbol] The encoding type.
|
|
10
12
|
attr_reader :encoding
|
|
11
13
|
|
|
12
|
-
# @return [String]
|
|
14
|
+
# @return [String] The encoded data.
|
|
13
15
|
attr_reader :encoded_value
|
|
14
16
|
|
|
15
|
-
# @return [String, nil]
|
|
17
|
+
# @return [String, nil] The character set (if applicable).
|
|
16
18
|
attr_reader :charset
|
|
17
19
|
|
|
18
20
|
class << self
|
|
19
21
|
# Reconstructs a Payload from a hash representation.
|
|
20
22
|
#
|
|
21
|
-
# @param hash [Hash, nil]
|
|
22
|
-
# @return [Payload, nil]
|
|
23
|
+
# @param hash [Hash, nil] Hash with "encoding" and "value" keys.
|
|
24
|
+
# @return [Payload, nil] Reconstructed payload or nil if hash is invalid.
|
|
23
25
|
def load(hash)
|
|
24
26
|
return nil if hash.nil? || hash["value"].nil?
|
|
25
27
|
|
|
@@ -33,9 +35,9 @@ module PatientHttp
|
|
|
33
35
|
# claims is text but that does not hold text is encoded as binary as
|
|
34
36
|
# well, because the serialized form must survive JSON encoding.
|
|
35
37
|
#
|
|
36
|
-
# @param value [String]
|
|
37
|
-
# @param mimetype [String, nil]
|
|
38
|
-
# @return [Array(Symbol, String, String), nil] [encoding, encoded_value, charset] or nil if value is nil
|
|
38
|
+
# @param value [String] The value to encode.
|
|
39
|
+
# @param mimetype [String, nil] The MIME type of the content.
|
|
40
|
+
# @return [Array(Symbol, String, String), nil] [encoding, encoded_value, charset] or nil if value is nil.
|
|
39
41
|
def encode(value, mimetype)
|
|
40
42
|
return nil if value.nil?
|
|
41
43
|
|
|
@@ -49,10 +51,10 @@ module PatientHttp
|
|
|
49
51
|
|
|
50
52
|
# Decodes an encoded value based on its encoding type.
|
|
51
53
|
#
|
|
52
|
-
# @param encoded_value [String]
|
|
53
|
-
# @param encoding [Symbol]
|
|
54
|
-
# @param charset [String, nil]
|
|
55
|
-
# @return [String, nil]
|
|
54
|
+
# @param encoded_value [String] The encoded data.
|
|
55
|
+
# @param encoding [Symbol] The encoding type (:text, :binary, :gzipped).
|
|
56
|
+
# @param charset [String, nil] The character set (if applicable).
|
|
57
|
+
# @return [String, nil] The decoded value or nil if encoded_value is nil.
|
|
56
58
|
def decode(encoded_value, encoding, charset)
|
|
57
59
|
return nil if encoded_value.nil?
|
|
58
60
|
|
|
@@ -70,10 +72,10 @@ module PatientHttp
|
|
|
70
72
|
|
|
71
73
|
private
|
|
72
74
|
|
|
73
|
-
#
|
|
75
|
+
# Encodes a text value, compressing it when that makes it smaller.
|
|
74
76
|
#
|
|
75
|
-
# @param value [String]
|
|
76
|
-
# @return [Array(Symbol, String, String)] [encoding, encoded_value, charset]
|
|
77
|
+
# @param value [String] The text to encode.
|
|
78
|
+
# @return [Array(Symbol, String, String)] [encoding, encoded_value, charset].
|
|
77
79
|
def encode_text(value)
|
|
78
80
|
return [:text, value, value.encoding.name] if value.bytesize < 4096
|
|
79
81
|
|
|
@@ -85,13 +87,13 @@ module PatientHttp
|
|
|
85
87
|
end
|
|
86
88
|
end
|
|
87
89
|
|
|
88
|
-
#
|
|
90
|
+
# Returns whether a value can be serialized as text. JSON encoding converts a
|
|
89
91
|
# string to UTF-8, so the value must either be valid text in its own
|
|
90
92
|
# encoding or hold bytes that are already valid UTF-8. A body still
|
|
91
93
|
# carrying a content encoding the reader could not decode holds neither,
|
|
92
94
|
# even though its MIME type names a text type.
|
|
93
95
|
#
|
|
94
|
-
# @param value [String]
|
|
96
|
+
# @param value [String] The value to check.
|
|
95
97
|
# @return [Boolean]
|
|
96
98
|
def text?(value)
|
|
97
99
|
return value.valid_encoding? unless value.encoding == Encoding::BINARY
|
|
@@ -117,7 +119,7 @@ module PatientHttp
|
|
|
117
119
|
end
|
|
118
120
|
end
|
|
119
121
|
|
|
120
|
-
#
|
|
122
|
+
# Returns the value as a UTF-8 encoded string if possible. If the value cannot
|
|
121
123
|
# be converted to UTF-8, return it in the response charset or ASCII-8BIT.
|
|
122
124
|
#
|
|
123
125
|
# Encoding strategy:
|
|
@@ -150,9 +152,9 @@ module PatientHttp
|
|
|
150
152
|
|
|
151
153
|
# Initializes a new Payload.
|
|
152
154
|
#
|
|
153
|
-
# @param encoding [Symbol]
|
|
154
|
-
# @param encoded_value [String]
|
|
155
|
-
# @param charset [String, nil]
|
|
155
|
+
# @param encoding [Symbol] The encoding type.
|
|
156
|
+
# @param encoded_value [String] The encoded data.
|
|
157
|
+
# @param charset [String, nil] The character set (if applicable).
|
|
156
158
|
def initialize(encoding, encoded_value, charset)
|
|
157
159
|
@encoded_value = encoded_value
|
|
158
160
|
@encoding = encoding
|
|
@@ -161,14 +163,14 @@ module PatientHttp
|
|
|
161
163
|
|
|
162
164
|
# Returns the decoded value.
|
|
163
165
|
#
|
|
164
|
-
# @return [String, nil]
|
|
166
|
+
# @return [String, nil] The decoded data.
|
|
165
167
|
def value
|
|
166
168
|
self.class.decode(encoded_value, encoding, charset)
|
|
167
169
|
end
|
|
168
170
|
|
|
169
171
|
# Converts to a hash representation for serialization.
|
|
170
172
|
#
|
|
171
|
-
# @return [Hash]
|
|
173
|
+
# @return [Hash] Hash with "encoding" and "value" keys.
|
|
172
174
|
def as_json
|
|
173
175
|
{
|
|
174
176
|
"encoding" => encoding.to_s,
|
|
@@ -4,32 +4,31 @@ require_relative "base"
|
|
|
4
4
|
|
|
5
5
|
module PatientHttp
|
|
6
6
|
module PayloadStore
|
|
7
|
-
#
|
|
7
|
+
# A payload store that keeps payloads as JSON in a database table. Use it in
|
|
8
|
+
# production when you want payloads in your database.
|
|
8
9
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
10
|
+
# Active Record is responsible for connection pooling and thread safety.
|
|
11
|
+
# The store requires the `patient_http_payloads` table.
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# @example Configuration
|
|
15
|
-
# require "patient_http/payload_store/active_record_store"
|
|
13
|
+
# @example Register an Active Record store
|
|
16
14
|
# config.register_payload_store(:database, adapter: :active_record)
|
|
17
15
|
#
|
|
18
|
-
# @example
|
|
16
|
+
# @example Register a store with a custom model
|
|
19
17
|
# config.register_payload_store(:database, adapter: :active_record,
|
|
20
18
|
# model: MyApp::PayloadRecord
|
|
21
19
|
# )
|
|
22
20
|
class ActiveRecordStore < Base
|
|
23
21
|
Base.register :active_record, self
|
|
24
22
|
|
|
25
|
-
#
|
|
23
|
+
# The default Active Record model for stored payloads.
|
|
26
24
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
25
|
+
# The model is defined in this file, so Active Record loads only when
|
|
26
|
+
# the store is used. Create the table with the migration from this gem.
|
|
29
27
|
#
|
|
30
|
-
# @example Install
|
|
31
|
-
# rails
|
|
32
|
-
# rails
|
|
28
|
+
# @example Install the migration in a Rails app
|
|
29
|
+
# # Add require "patient_http/rails/engine" to an initializer.
|
|
30
|
+
# bin/rails patient_http:install:migrations
|
|
31
|
+
# bin/rails db:migrate
|
|
33
32
|
class Payload < ::ActiveRecord::Base
|
|
34
33
|
self.table_name = "patient_http_payloads"
|
|
35
34
|
self.primary_key = "key"
|
|
@@ -37,23 +36,23 @@ module PatientHttp
|
|
|
37
36
|
scope :older_than, ->(time) { where(created_at: nil...time) }
|
|
38
37
|
end
|
|
39
38
|
|
|
40
|
-
# @return [Class] The
|
|
39
|
+
# @return [Class] The Active Record model for stored payloads.
|
|
41
40
|
attr_reader :model
|
|
42
41
|
|
|
43
|
-
#
|
|
42
|
+
# Creates an Active Record store.
|
|
44
43
|
#
|
|
45
|
-
# @param model [Class]
|
|
46
|
-
#
|
|
47
|
-
#
|
|
44
|
+
# @param model [Class, nil] The Active Record model. If `nil`, {Payload}
|
|
45
|
+
# applies. A custom model must have a `key` string primary key, a `data`
|
|
46
|
+
# text column, and timestamps.
|
|
48
47
|
def initialize(model: nil)
|
|
49
48
|
@model = model || Payload
|
|
50
49
|
end
|
|
51
50
|
|
|
52
|
-
#
|
|
51
|
+
# Stores a JSON string in the database.
|
|
53
52
|
#
|
|
54
|
-
# @param key [String]
|
|
55
|
-
# @param json [String]
|
|
56
|
-
# @return [String] The key
|
|
53
|
+
# @param key [String] The unique key. The primary key is this key.
|
|
54
|
+
# @param json [String] The serialized JSON.
|
|
55
|
+
# @return [String] The key.
|
|
57
56
|
def store_json(key, json)
|
|
58
57
|
now = Time.current
|
|
59
58
|
|
|
@@ -68,10 +67,10 @@ module PatientHttp
|
|
|
68
67
|
key
|
|
69
68
|
end
|
|
70
69
|
|
|
71
|
-
#
|
|
70
|
+
# Fetches stored data.
|
|
72
71
|
#
|
|
73
|
-
# @param key [String] The key
|
|
74
|
-
# @return [Hash, nil] The
|
|
72
|
+
# @param key [String] The key.
|
|
73
|
+
# @return [Hash, nil] The parsed data, or `nil` if the key isn't found.
|
|
75
74
|
def fetch(key)
|
|
76
75
|
record = @model.find_by(key: key)
|
|
77
76
|
return nil unless record
|
|
@@ -79,21 +78,19 @@ module PatientHttp
|
|
|
79
78
|
JSON.parse(record.data)
|
|
80
79
|
end
|
|
81
80
|
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
# Idempotent - does not raise if record doesn't exist.
|
|
81
|
+
# Deletes stored data. Doesn't raise an error if the key doesn't exist.
|
|
85
82
|
#
|
|
86
|
-
# @param key [String] The key
|
|
87
|
-
# @return [Boolean] true
|
|
83
|
+
# @param key [String] The key.
|
|
84
|
+
# @return [Boolean] `true`.
|
|
88
85
|
def delete(key)
|
|
89
86
|
@model.where(key: key).delete_all
|
|
90
87
|
true
|
|
91
88
|
end
|
|
92
89
|
|
|
93
|
-
#
|
|
90
|
+
# Returns whether a payload exists.
|
|
94
91
|
#
|
|
95
|
-
# @param key [String] The key
|
|
96
|
-
# @return [Boolean] true if the payload exists
|
|
92
|
+
# @param key [String] The key.
|
|
93
|
+
# @return [Boolean] `true` if the payload exists.
|
|
97
94
|
def exists?(key)
|
|
98
95
|
@model.exists?(key: key)
|
|
99
96
|
end
|
|
@@ -4,15 +4,15 @@ require "securerandom"
|
|
|
4
4
|
|
|
5
5
|
module PatientHttp
|
|
6
6
|
module PayloadStore
|
|
7
|
-
#
|
|
7
|
+
# The abstract base class for payload stores.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
# that
|
|
11
|
-
# small while allowing large payloads to be processed.
|
|
9
|
+
# A payload store holds serialized payloads that are larger than
|
|
10
|
+
# `payload_store_threshold`, so that job arguments stay small.
|
|
12
11
|
#
|
|
13
|
-
# Subclasses must implement
|
|
12
|
+
# Subclasses must implement {#store_json}, {#fetch}, and {#delete}, and be
|
|
13
|
+
# thread-safe.
|
|
14
14
|
#
|
|
15
|
-
# @example
|
|
15
|
+
# @example Create a custom store
|
|
16
16
|
# class MyStore < PatientHttp::PayloadStore::Base
|
|
17
17
|
# register :my_store, self
|
|
18
18
|
#
|
|
@@ -21,8 +21,8 @@ module PatientHttp
|
|
|
21
21
|
# @mutex = Mutex.new
|
|
22
22
|
# end
|
|
23
23
|
#
|
|
24
|
-
# def
|
|
25
|
-
# @mutex.synchronize { @connection.set(key,
|
|
24
|
+
# def store_json(key, json)
|
|
25
|
+
# @mutex.synchronize { @connection.set(key, json) }
|
|
26
26
|
# key
|
|
27
27
|
# end
|
|
28
28
|
#
|
|
@@ -41,10 +41,10 @@ module PatientHttp
|
|
|
41
41
|
# end
|
|
42
42
|
class Base
|
|
43
43
|
class << self
|
|
44
|
-
#
|
|
44
|
+
# Registers a payload store adapter.
|
|
45
45
|
#
|
|
46
|
-
# @param name [Symbol]
|
|
47
|
-
# @param klass [Class] The adapter class
|
|
46
|
+
# @param name [Symbol] The unique adapter name.
|
|
47
|
+
# @param klass [Class] The adapter class.
|
|
48
48
|
# @return [void]
|
|
49
49
|
def register(name, klass)
|
|
50
50
|
registry_mutex.synchronize do
|
|
@@ -52,22 +52,22 @@ module PatientHttp
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
#
|
|
55
|
+
# Returns a registered adapter class.
|
|
56
56
|
#
|
|
57
|
-
# @param name [Symbol, String] The adapter name
|
|
58
|
-
# @return [Class, nil] The adapter class or nil if
|
|
57
|
+
# @param name [Symbol, String] The adapter name.
|
|
58
|
+
# @return [Class, nil] The adapter class, or `nil` if it isn't registered.
|
|
59
59
|
def lookup(name)
|
|
60
60
|
registry_mutex.synchronize do
|
|
61
61
|
registry[name.to_sym]
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
#
|
|
65
|
+
# Creates a store from a registered adapter.
|
|
66
66
|
#
|
|
67
|
-
# @param name [Symbol, String] The adapter name
|
|
68
|
-
# @param options [Hash]
|
|
69
|
-
# @return [Base]
|
|
70
|
-
# @raise [ArgumentError] If the adapter
|
|
67
|
+
# @param name [Symbol, String] The adapter name.
|
|
68
|
+
# @param options [Hash] The options for the adapter constructor.
|
|
69
|
+
# @return [Base] The store.
|
|
70
|
+
# @raise [ArgumentError] If the adapter isn't registered.
|
|
71
71
|
def create(name, **options)
|
|
72
72
|
klass = lookup(name)
|
|
73
73
|
raise ArgumentError, "Unknown payload store adapter: #{name.inspect}" unless klass
|
|
@@ -75,9 +75,9 @@ module PatientHttp
|
|
|
75
75
|
klass.new(**options)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
#
|
|
78
|
+
# Returns the names of all registered adapters.
|
|
79
79
|
#
|
|
80
|
-
# @return [Array<Symbol>]
|
|
80
|
+
# @return [Array<Symbol>] The adapter names.
|
|
81
81
|
def registered_adapters
|
|
82
82
|
registry_mutex.synchronize do
|
|
83
83
|
registry.keys
|
|
@@ -95,53 +95,49 @@ module PatientHttp
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
#
|
|
98
|
+
# Stores a hash as JSON. This method calls {#store_json}.
|
|
99
99
|
#
|
|
100
|
-
# @param key [String]
|
|
101
|
-
# @param data [Hash] The data to store
|
|
102
|
-
# @return [String] The key
|
|
100
|
+
# @param key [String] The unique key.
|
|
101
|
+
# @param data [Hash] The data to store.
|
|
102
|
+
# @return [String] The key.
|
|
103
103
|
def store(key, data)
|
|
104
104
|
json = JSON.generate(data)
|
|
105
105
|
store_json(key, json)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
#
|
|
108
|
+
# Stores a JSON string. {ExternalStorage} calls this method, so
|
|
109
|
+
# subclasses must implement it.
|
|
109
110
|
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
# @
|
|
114
|
-
# @param json [String] Pre-serialized JSON string
|
|
115
|
-
# @return [String] The key
|
|
116
|
-
# @raise [NotImplementedError] Subclasses must implement this method
|
|
111
|
+
# @param key [String] The unique key.
|
|
112
|
+
# @param json [String] The serialized JSON.
|
|
113
|
+
# @return [String] The key.
|
|
114
|
+
# @raise [NotImplementedError] If the subclass doesn't implement it.
|
|
117
115
|
def store_json(key, json)
|
|
118
116
|
raise NotImplementedError, "#{self.class.name} must implement #store_json"
|
|
119
117
|
end
|
|
120
118
|
|
|
121
|
-
#
|
|
119
|
+
# Fetches stored data.
|
|
122
120
|
#
|
|
123
|
-
# @param key [String] The key
|
|
124
|
-
# @return [Hash, nil] The
|
|
125
|
-
# @raise [NotImplementedError]
|
|
121
|
+
# @param key [String] The key.
|
|
122
|
+
# @return [Hash, nil] The parsed data, or `nil` if the key isn't found.
|
|
123
|
+
# @raise [NotImplementedError] If the subclass doesn't implement it.
|
|
126
124
|
def fetch(key)
|
|
127
125
|
raise NotImplementedError, "#{self.class.name} must implement #fetch"
|
|
128
126
|
end
|
|
129
127
|
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# This method should be idempotent - deleting a non-existent key
|
|
133
|
-
# should not raise an error.
|
|
128
|
+
# Deletes stored data. The method must be idempotent: deleting a key that
|
|
129
|
+
# doesn't exist must not raise an error.
|
|
134
130
|
#
|
|
135
|
-
# @param key [String] The key
|
|
136
|
-
# @return [Boolean] true
|
|
137
|
-
# @raise [NotImplementedError]
|
|
131
|
+
# @param key [String] The key.
|
|
132
|
+
# @return [Boolean] `true`.
|
|
133
|
+
# @raise [NotImplementedError] If the subclass doesn't implement it.
|
|
138
134
|
def delete(key)
|
|
139
135
|
raise NotImplementedError, "#{self.class.name} must implement #delete"
|
|
140
136
|
end
|
|
141
137
|
|
|
142
|
-
#
|
|
138
|
+
# Generates a unique key.
|
|
143
139
|
#
|
|
144
|
-
# @return [String] A UUID
|
|
140
|
+
# @return [String] A UUID.
|
|
145
141
|
def generate_key
|
|
146
142
|
SecureRandom.uuid
|
|
147
143
|
end
|
|
@@ -4,37 +4,35 @@ require "fileutils"
|
|
|
4
4
|
|
|
5
5
|
module PatientHttp
|
|
6
6
|
module PayloadStore
|
|
7
|
-
#
|
|
7
|
+
# A payload store that keeps payloads as JSON files in a directory. Use it
|
|
8
|
+
# only for development and tests, because hosts don't share the files. In
|
|
9
|
+
# production, use another store.
|
|
8
10
|
#
|
|
9
|
-
#
|
|
10
|
-
# for local development and testing only - use Redis or S3 stores for
|
|
11
|
-
# production deployments.
|
|
11
|
+
# The store is thread-safe.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @example Configuration
|
|
13
|
+
# @example Register a file store
|
|
16
14
|
# config.register_payload_store(:files, adapter: :file, directory: "/tmp/payloads")
|
|
17
15
|
class FileStore < Base
|
|
18
16
|
Base.register :file, self
|
|
19
17
|
|
|
20
|
-
# @return [String] The directory
|
|
18
|
+
# @return [String] The directory for the payload files.
|
|
21
19
|
attr_reader :directory
|
|
22
20
|
|
|
23
|
-
#
|
|
21
|
+
# Creates a file store.
|
|
24
22
|
#
|
|
25
|
-
# @param directory [String]
|
|
26
|
-
#
|
|
23
|
+
# @param directory [String] The directory for the payload files. The
|
|
24
|
+
# default is `Dir.tmpdir`. The directory is created if it doesn't exist.
|
|
27
25
|
def initialize(directory: nil)
|
|
28
26
|
@directory = directory || Dir.tmpdir
|
|
29
27
|
@mutex = Mutex.new
|
|
30
28
|
FileUtils.mkdir_p(@directory)
|
|
31
29
|
end
|
|
32
30
|
|
|
33
|
-
#
|
|
31
|
+
# Writes a JSON string to a file.
|
|
34
32
|
#
|
|
35
|
-
# @param key [String]
|
|
36
|
-
# @param json [String]
|
|
37
|
-
# @return [String] The key
|
|
33
|
+
# @param key [String] The unique key. The file name is the key.
|
|
34
|
+
# @param json [String] The serialized JSON.
|
|
35
|
+
# @return [String] The key.
|
|
38
36
|
def store_json(key, json)
|
|
39
37
|
path = file_path(key)
|
|
40
38
|
@mutex.synchronize do
|
|
@@ -43,10 +41,10 @@ module PatientHttp
|
|
|
43
41
|
key
|
|
44
42
|
end
|
|
45
43
|
|
|
46
|
-
#
|
|
44
|
+
# Fetches stored data.
|
|
47
45
|
#
|
|
48
|
-
# @param key [String] The key
|
|
49
|
-
# @return [Hash, nil] The
|
|
46
|
+
# @param key [String] The key.
|
|
47
|
+
# @return [Hash, nil] The parsed data, or `nil` if the key isn't found.
|
|
50
48
|
def fetch(key)
|
|
51
49
|
path = file_path(key)
|
|
52
50
|
@mutex.synchronize do
|
|
@@ -56,12 +54,10 @@ module PatientHttp
|
|
|
56
54
|
end
|
|
57
55
|
end
|
|
58
56
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
# Idempotent - does not raise if file doesn't exist.
|
|
57
|
+
# Deletes stored data. Doesn't raise an error if the key doesn't exist.
|
|
62
58
|
#
|
|
63
|
-
# @param key [String] The key
|
|
64
|
-
# @return [Boolean] true
|
|
59
|
+
# @param key [String] The key.
|
|
60
|
+
# @return [Boolean] `true`.
|
|
65
61
|
def delete(key)
|
|
66
62
|
path = file_path(key)
|
|
67
63
|
@mutex.synchronize do
|
|
@@ -72,10 +68,10 @@ module PatientHttp
|
|
|
72
68
|
true
|
|
73
69
|
end
|
|
74
70
|
|
|
75
|
-
#
|
|
71
|
+
# Returns whether a payload exists.
|
|
76
72
|
#
|
|
77
|
-
# @param key [String] The key
|
|
78
|
-
# @return [Boolean] true if the payload exists
|
|
73
|
+
# @param key [String] The key.
|
|
74
|
+
# @return [Boolean] `true` if the payload exists.
|
|
79
75
|
def exists?(key)
|
|
80
76
|
@mutex.synchronize do
|
|
81
77
|
File.exist?(file_path(key))
|
|
@@ -2,37 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
4
|
module PayloadStore
|
|
5
|
-
#
|
|
5
|
+
# A payload store that keeps payloads as JSON strings in Redis. Use it in
|
|
6
|
+
# production when several processes share payloads.
|
|
6
7
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# payload data.
|
|
8
|
+
# The client must respond to `set`, `get`, `del`, and `exists`, like a client
|
|
9
|
+
# from the `redis` gem. The client is responsible for thread safety.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# The client must respond to `set`, `get`, `del`, and `exists` (the
|
|
14
|
-
# interface provided by the `redis` gem).
|
|
15
|
-
#
|
|
16
|
-
# @example Configuration with direct Redis client
|
|
11
|
+
# @example Register a Redis store
|
|
17
12
|
# redis = Redis.new(url: ENV["REDIS_URL"])
|
|
18
13
|
# config.register_payload_store(:redis, adapter: :redis, redis: redis, ttl: 86400)
|
|
19
14
|
class RedisStore < Base
|
|
20
15
|
Base.register :redis, self
|
|
21
16
|
|
|
22
|
-
# @return [String] The
|
|
17
|
+
# @return [String] The prefix for the keys of all stored payloads.
|
|
23
18
|
attr_reader :key_prefix
|
|
24
19
|
|
|
25
|
-
# @return [Float, nil]
|
|
20
|
+
# @return [Float, nil] The time to live in seconds for stored payloads.
|
|
26
21
|
attr_reader :ttl
|
|
27
22
|
|
|
28
|
-
#
|
|
23
|
+
# Creates a Redis store.
|
|
29
24
|
#
|
|
30
|
-
# @param redis [Object] Redis client
|
|
31
|
-
# @param ttl [Float, nil]
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# @raise [ArgumentError] If
|
|
25
|
+
# @param redis [Object] The Redis client.
|
|
26
|
+
# @param ttl [Float, nil] The time to live in seconds for stored payloads.
|
|
27
|
+
# Fractions are allowed, for example `0.5` for 500 milliseconds. If `nil`,
|
|
28
|
+
# payloads don't expire.
|
|
29
|
+
# @param key_prefix [String] The prefix for all Redis keys.
|
|
30
|
+
# @raise [ArgumentError] If the Redis client is missing.
|
|
36
31
|
def initialize(redis:, ttl: nil, key_prefix: nil)
|
|
37
32
|
raise ArgumentError, "redis client is required" unless redis
|
|
38
33
|
|
|
@@ -41,11 +36,12 @@ module PatientHttp
|
|
|
41
36
|
@key_prefix = key_prefix || "patient_http:payloads:"
|
|
42
37
|
end
|
|
43
38
|
|
|
44
|
-
#
|
|
39
|
+
# Stores a JSON string in Redis.
|
|
45
40
|
#
|
|
46
|
-
# @param key [String]
|
|
47
|
-
#
|
|
48
|
-
# @
|
|
41
|
+
# @param key [String] The unique key. The Redis key is the key prefix and
|
|
42
|
+
# this key.
|
|
43
|
+
# @param json [String] The serialized JSON.
|
|
44
|
+
# @return [String] The key.
|
|
49
45
|
def store_json(key, json)
|
|
50
46
|
full_key = key_with_prefix(key)
|
|
51
47
|
|
|
@@ -58,10 +54,10 @@ module PatientHttp
|
|
|
58
54
|
key
|
|
59
55
|
end
|
|
60
56
|
|
|
61
|
-
#
|
|
57
|
+
# Fetches stored data.
|
|
62
58
|
#
|
|
63
|
-
# @param key [String] The key
|
|
64
|
-
# @return [Hash, nil] The
|
|
59
|
+
# @param key [String] The key.
|
|
60
|
+
# @return [Hash, nil] The parsed data, or `nil` if the key isn't found.
|
|
65
61
|
def fetch(key)
|
|
66
62
|
full_key = key_with_prefix(key)
|
|
67
63
|
json = @redis.get(full_key)
|
|
@@ -70,22 +66,20 @@ module PatientHttp
|
|
|
70
66
|
JSON.parse(json)
|
|
71
67
|
end
|
|
72
68
|
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
# Idempotent - does not raise if key doesn't exist.
|
|
69
|
+
# Deletes stored data. Doesn't raise an error if the key doesn't exist.
|
|
76
70
|
#
|
|
77
|
-
# @param key [String] The key
|
|
78
|
-
# @return [Boolean] true
|
|
71
|
+
# @param key [String] The key.
|
|
72
|
+
# @return [Boolean] `true`.
|
|
79
73
|
def delete(key)
|
|
80
74
|
full_key = key_with_prefix(key)
|
|
81
75
|
@redis.del(full_key)
|
|
82
76
|
true
|
|
83
77
|
end
|
|
84
78
|
|
|
85
|
-
#
|
|
79
|
+
# Returns whether a payload exists.
|
|
86
80
|
#
|
|
87
|
-
# @param key [String] The key
|
|
88
|
-
# @return [Boolean] true if the payload exists
|
|
81
|
+
# @param key [String] The key.
|
|
82
|
+
# @return [Boolean] `true` if the payload exists.
|
|
89
83
|
def exists?(key)
|
|
90
84
|
full_key = key_with_prefix(key)
|
|
91
85
|
@redis.exists(full_key) > 0
|