patient_http 1.6.0 → 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 +39 -0
- data/README.md +540 -514
- 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 +57 -26
- 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 +20 -4
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
# A reference to a named secret
|
|
5
|
-
#
|
|
4
|
+
# A reference to a named secret. Use it as a header or query parameter value
|
|
5
|
+
# in a {Request}. Create one with {PatientHttp.secret}.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# the {Configuration}.
|
|
7
|
+
# The reference holds only the name of the secret, not its value. A
|
|
8
|
+
# serialized request stores the marker `{"$secret" => name}`, so the value
|
|
9
|
+
# isn't written to the job queue or to logs. The processor resolves the value
|
|
10
|
+
# from the secrets registered in the {Configuration} when it sends the
|
|
11
|
+
# request.
|
|
13
12
|
#
|
|
14
|
-
# @example
|
|
13
|
+
# @example Refer to secrets in a request
|
|
15
14
|
# PatientHttp.get(
|
|
16
15
|
# "https://api.example.com/data",
|
|
17
16
|
# callback: MyCallback,
|
|
@@ -19,28 +18,28 @@ module PatientHttp
|
|
|
19
18
|
# params: {"api_key" => PatientHttp.secret(:api_key)}
|
|
20
19
|
# )
|
|
21
20
|
class SecretReference
|
|
22
|
-
#
|
|
21
|
+
# The key that identifies a secret reference in serialized JSON.
|
|
23
22
|
REFERENCE_KEY = "$secret"
|
|
24
23
|
|
|
25
|
-
# @return [String]
|
|
24
|
+
# @return [String] The secret name.
|
|
26
25
|
attr_reader :name
|
|
27
26
|
|
|
28
27
|
class << self
|
|
29
|
-
#
|
|
30
|
-
# serialized marker hash
|
|
28
|
+
# Returns whether a value is a secret reference. The value can be a
|
|
29
|
+
# `SecretReference` or a serialized marker hash.
|
|
31
30
|
#
|
|
32
|
-
# @param value [Object]
|
|
33
|
-
# @return [Boolean] true if the value is a secret reference
|
|
31
|
+
# @param value [Object] The value to check.
|
|
32
|
+
# @return [Boolean] `true` if the value is a secret reference.
|
|
34
33
|
def reference?(value)
|
|
35
34
|
value.is_a?(SecretReference) ||
|
|
36
35
|
(value.is_a?(Hash) && value.key?(REFERENCE_KEY))
|
|
37
36
|
end
|
|
38
37
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
38
|
+
# Creates a reference from a serialized marker hash. Other values are
|
|
39
|
+
# returned unchanged.
|
|
41
40
|
#
|
|
42
|
-
# @param value [Object]
|
|
43
|
-
# @return [Object]
|
|
41
|
+
# @param value [Object] A serialized marker hash, or any other value.
|
|
42
|
+
# @return [Object] A `SecretReference` for a marker hash, or the value.
|
|
44
43
|
def load(value)
|
|
45
44
|
return value unless value.is_a?(Hash) && value.key?(REFERENCE_KEY)
|
|
46
45
|
|
|
@@ -48,34 +47,42 @@ module PatientHttp
|
|
|
48
47
|
end
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
#
|
|
50
|
+
# Creates a reference.
|
|
52
51
|
#
|
|
53
|
-
# @param name [String, Symbol]
|
|
54
|
-
# @raise [ArgumentError]
|
|
52
|
+
# @param name [String, Symbol] The secret name.
|
|
53
|
+
# @raise [ArgumentError] If the name is empty.
|
|
55
54
|
def initialize(name)
|
|
56
55
|
@name = name.to_s
|
|
57
56
|
raise ArgumentError.new("secret name cannot be empty") if @name.empty?
|
|
58
57
|
end
|
|
59
58
|
|
|
60
|
-
#
|
|
59
|
+
# Returns the reference as a marker hash. The hash has the name, not the
|
|
60
|
+
# value.
|
|
61
61
|
#
|
|
62
|
-
# @return [Hash]
|
|
62
|
+
# @return [Hash] The marker hash.
|
|
63
63
|
def as_json
|
|
64
64
|
{REFERENCE_KEY => name}
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# Returns whether another object is a reference to the same secret.
|
|
68
|
+
#
|
|
69
|
+
# @param other [Object] The object to compare.
|
|
70
|
+
# @return [Boolean] `true` if the names are equal.
|
|
67
71
|
def ==(other)
|
|
68
72
|
other.is_a?(SecretReference) && other.name == name
|
|
69
73
|
end
|
|
70
74
|
alias_method :eql?, :==
|
|
71
75
|
|
|
76
|
+
# Returns a hash code based on the secret name.
|
|
77
|
+
#
|
|
78
|
+
# @return [Integer] The hash code.
|
|
72
79
|
def hash
|
|
73
80
|
[self.class, name].hash
|
|
74
81
|
end
|
|
75
82
|
|
|
76
|
-
#
|
|
83
|
+
# Returns a description of the reference. It shows only the name.
|
|
77
84
|
#
|
|
78
|
-
# @return [String]
|
|
85
|
+
# @return [String] The description.
|
|
79
86
|
def inspect
|
|
80
87
|
"#<PatientHttp::SecretReference name=#{name.inspect}>"
|
|
81
88
|
end
|
|
@@ -1,77 +1,60 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# Runs a request synchronously on the calling thread, and then calls the
|
|
5
|
+
# callback service.
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Use it in tests, and wherever a request must run inline. It doesn't depend
|
|
8
|
+
# on module-level state: you give it the configuration and optional hooks.
|
|
9
|
+
#
|
|
10
|
+
# The request uses a {ClientPool} that exists only for this call. The
|
|
11
|
+
# connection timeout, TCP settings, protocol, proxy, and retry rules are the
|
|
12
|
+
# same as for requests on a processor.
|
|
13
|
+
#
|
|
14
|
+
# @example Run a request
|
|
15
|
+
# executor = PatientHttp::SynchronousExecutor.new(
|
|
16
|
+
# task,
|
|
17
|
+
# config: config,
|
|
18
|
+
# on_complete: ->(response) { StatsD.increment("complete") },
|
|
19
|
+
# on_error: ->(error) { StatsD.increment("error") }
|
|
20
|
+
# )
|
|
21
|
+
# executor.call
|
|
9
22
|
class SynchronousExecutor
|
|
10
23
|
include RedirectHelper
|
|
24
|
+
include ImmediateRetries
|
|
11
25
|
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# @param
|
|
15
|
-
# @param
|
|
26
|
+
# Creates an executor.
|
|
27
|
+
#
|
|
28
|
+
# @param task [RequestTask] The task to run.
|
|
29
|
+
# @param config [Configuration] The configuration for the request.
|
|
30
|
+
# @param on_complete [Proc, nil] A hook that runs with the response before
|
|
31
|
+
# the callback service's `on_complete` method.
|
|
32
|
+
# @param on_error [Proc, nil] A hook that runs with the error before the
|
|
33
|
+
# callback service's `on_error` method.
|
|
16
34
|
def initialize(task, config:, on_complete: nil, on_error: nil)
|
|
17
35
|
@task = task
|
|
18
36
|
@config = config
|
|
19
37
|
@on_complete = on_complete
|
|
20
38
|
@on_error = on_error
|
|
21
|
-
@proxy_client = nil
|
|
22
39
|
@request_preparer = RequestPreparer.new(config)
|
|
23
40
|
@response_reader = ResponseReader.new(nil, config: config)
|
|
41
|
+
@client_pool = ClientPool.from_config(config)
|
|
24
42
|
end
|
|
25
43
|
|
|
26
|
-
#
|
|
44
|
+
# Runs the request, and then calls the hook and the callback service with
|
|
45
|
+
# the response or error.
|
|
46
|
+
#
|
|
27
47
|
# @return [void]
|
|
28
48
|
def call
|
|
29
49
|
Async do
|
|
30
50
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
51
|
|
|
32
52
|
begin
|
|
33
|
-
http_client = nil
|
|
34
53
|
response_data = nil
|
|
35
54
|
redirect_error = nil
|
|
36
55
|
|
|
37
56
|
loop do
|
|
38
|
-
|
|
39
|
-
@proxy_client&.close
|
|
40
|
-
@proxy_client = nil
|
|
41
|
-
outgoing = @request_preparer.prepare(@task.request, @task.id)
|
|
42
|
-
http_client = create_http_client(outgoing.url)
|
|
43
|
-
timeout = @task.request.timeout || @config.request_timeout
|
|
44
|
-
|
|
45
|
-
response_data = Async::Task.current.with_timeout(timeout) do
|
|
46
|
-
headers = outgoing.headers.to_h
|
|
47
|
-
body = Protocol::HTTP::Body::Buffered.wrap([@task.request.body.to_s]) if @task.request.body
|
|
48
|
-
|
|
49
|
-
endpoint = Async::HTTP::Endpoint.parse(outgoing.url)
|
|
50
|
-
endpoint = configure_endpoint(endpoint) if @config.connection_timeout
|
|
51
|
-
|
|
52
|
-
verb = @task.request.http_method.to_s.upcase
|
|
53
|
-
options = {
|
|
54
|
-
headers: headers,
|
|
55
|
-
body: body,
|
|
56
|
-
scheme: endpoint.scheme,
|
|
57
|
-
authority: endpoint.authority
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
request = Protocol::HTTP::Request[verb, endpoint.path, **options]
|
|
61
|
-
async_response = http_client.call(request)
|
|
62
|
-
# Note: headers that appear multiple times (e.g. set-cookie) are
|
|
63
|
-
# flattened to a single joined string value.
|
|
64
|
-
headers_hash = async_response.headers.to_h.transform_values(&:to_s)
|
|
65
|
-
|
|
66
|
-
chunks = @response_reader.read_raw_body(async_response, headers_hash)
|
|
67
|
-
body_content = @response_reader.decode_body(chunks, headers_hash)
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
status: async_response.status,
|
|
71
|
-
headers: ResponseReader.rewrite_content_encoding(headers_hash),
|
|
72
|
-
body: body_content
|
|
73
|
-
}
|
|
74
|
-
end
|
|
57
|
+
response_data = perform_request
|
|
75
58
|
|
|
76
59
|
# Check for redirect
|
|
77
60
|
break unless should_follow_redirect?(@task, response_data)
|
|
@@ -123,65 +106,54 @@ module PatientHttp
|
|
|
123
106
|
)
|
|
124
107
|
invoke_callback(error, :error)
|
|
125
108
|
ensure
|
|
126
|
-
|
|
127
|
-
@proxy_client&.close
|
|
128
|
-
@proxy_client = nil
|
|
109
|
+
@client_pool.close
|
|
129
110
|
end
|
|
130
111
|
end
|
|
131
112
|
end
|
|
132
113
|
|
|
133
114
|
private
|
|
134
115
|
|
|
135
|
-
|
|
136
|
-
#
|
|
137
|
-
# The client is not wrapped in a Protocol::HTTP::AcceptEncoding middleware.
|
|
138
|
-
# That wrapper overwrites the request's accept-encoding header, which would
|
|
139
|
-
# ignore a caller opting out of compression, so response bodies are decoded
|
|
140
|
-
# by ResponseReader here exactly as they are on the async path.
|
|
141
|
-
#
|
|
142
|
-
# @param url [String] the resolved request URL
|
|
143
|
-
# @return [Async::HTTP::Client] the HTTP client
|
|
144
|
-
def create_http_client(url)
|
|
145
|
-
endpoint = Async::HTTP::Endpoint.parse(url)
|
|
146
|
-
endpoint = configure_endpoint(endpoint) if @config.connection_timeout
|
|
147
|
-
|
|
148
|
-
if @config.proxy_url
|
|
149
|
-
create_proxied_client(endpoint)
|
|
150
|
-
else
|
|
151
|
-
Async::HTTP::Client.new(endpoint, retries: @config.retries)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
116
|
+
attr_reader :config
|
|
154
117
|
|
|
155
|
-
#
|
|
118
|
+
# Sends the request of the current task and reads the full response.
|
|
156
119
|
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
require "async/http/proxy"
|
|
161
|
-
|
|
162
|
-
proxy_endpoint = Async::HTTP::Endpoint.parse(@config.proxy_url)
|
|
163
|
-
proxy_endpoint = configure_endpoint(proxy_endpoint) if @config.connection_timeout
|
|
164
|
-
@proxy_client = Async::HTTP::Client.new(proxy_endpoint)
|
|
165
|
-
|
|
166
|
-
proxy = @proxy_client.proxy(endpoint)
|
|
167
|
-
Async::HTTP::Client.new(proxy.wrap_endpoint(endpoint), retries: @config.retries)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Configure endpoint with connection timeout if specified.
|
|
120
|
+
# {ResponseReader} decodes the body. A `Protocol::HTTP::AcceptEncoding`
|
|
121
|
+
# wrapper isn't used, because it replaces an `accept-encoding` header that
|
|
122
|
+
# the caller set to turn off compression.
|
|
171
123
|
#
|
|
172
|
-
# @
|
|
173
|
-
#
|
|
174
|
-
def
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
)
|
|
124
|
+
# @return [Hash] The response data, with the `:status`, `:headers`, and
|
|
125
|
+
# `:body` keys.
|
|
126
|
+
def perform_request
|
|
127
|
+
outgoing = @request_preparer.prepare(@task.request, @task.id)
|
|
128
|
+
timeout = @task.request.timeout || @config.request_timeout
|
|
129
|
+
|
|
130
|
+
Async::Task.current.with_timeout(timeout) do
|
|
131
|
+
headers = outgoing.headers.to_h
|
|
132
|
+
body = Protocol::HTTP::Body::Buffered.wrap([@task.request.body.to_s]) if @task.request.body
|
|
133
|
+
|
|
134
|
+
endpoint = Async::HTTP::Endpoint.parse(outgoing.url)
|
|
135
|
+
async_response = request_with_immediate_retries(
|
|
136
|
+
@client_pool, @task.request, endpoint, headers, body
|
|
137
|
+
)
|
|
138
|
+
# Note: headers that appear multiple times (e.g. set-cookie) are
|
|
139
|
+
# flattened to a single joined string value.
|
|
140
|
+
headers_hash = async_response.headers.to_h.transform_values(&:to_s)
|
|
141
|
+
|
|
142
|
+
chunks = @response_reader.read_raw_body(async_response, headers_hash)
|
|
143
|
+
body_content = @response_reader.decode_body(chunks, headers_hash)
|
|
144
|
+
|
|
145
|
+
{
|
|
146
|
+
status: async_response.status,
|
|
147
|
+
headers: ResponseReader.rewrite_content_encoding(headers_hash),
|
|
148
|
+
body: body_content
|
|
149
|
+
}
|
|
150
|
+
end
|
|
179
151
|
end
|
|
180
152
|
|
|
181
|
-
#
|
|
153
|
+
# Calls the hook and the callback service with a result.
|
|
182
154
|
#
|
|
183
|
-
# @param result [Response, Error]
|
|
184
|
-
# @param type [Symbol]
|
|
155
|
+
# @param result [Response, Error] The result.
|
|
156
|
+
# @param type [Symbol] `:response` or `:error`.
|
|
185
157
|
def invoke_callback(result, type)
|
|
186
158
|
callback_class = @task.callback.is_a?(Class) ? @task.callback : ClassHelper.resolve_class_name(@task.callback)
|
|
187
159
|
callback = callback_class.new
|
|
@@ -1,53 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# The abstract base class that connects a {RequestTask} to a job system.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# The processor calls the task handler to deliver a result and to re-enqueue
|
|
7
|
+
# a request that didn't finish. Because of this class, {RequestTask} doesn't
|
|
8
|
+
# depend on a job system.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# The processor calls {#on_complete} and {#on_error} on its completion worker
|
|
11
|
+
# threads. The methods must be thread-safe and idempotent, and they should
|
|
12
|
+
# return quickly, for example after they enqueue a job.
|
|
11
13
|
#
|
|
12
|
-
# @
|
|
14
|
+
# @abstract Subclass it and implement all methods.
|
|
15
|
+
#
|
|
16
|
+
# @example Create a task handler
|
|
13
17
|
# class MyTaskHandler < PatientHttp::TaskHandler
|
|
14
18
|
# def on_complete(response, callback)
|
|
15
|
-
#
|
|
19
|
+
# MyJobSystem.enqueue(callback, :on_complete, response.as_json)
|
|
16
20
|
# end
|
|
17
21
|
#
|
|
18
22
|
# def on_error(error, callback)
|
|
19
|
-
#
|
|
23
|
+
# MyJobSystem.enqueue(callback, :on_error, error.as_json)
|
|
20
24
|
# end
|
|
21
25
|
#
|
|
22
26
|
# def retry
|
|
23
|
-
#
|
|
27
|
+
# MyJobSystem.enqueue_job(@job_id)
|
|
24
28
|
# end
|
|
25
29
|
# end
|
|
26
30
|
class TaskHandler
|
|
27
|
-
#
|
|
31
|
+
# Delivers a response to the callback service's `on_complete` method.
|
|
28
32
|
#
|
|
29
|
-
# @param response [Response]
|
|
30
|
-
# @param callback [String] callback class name
|
|
33
|
+
# @param response [Response] The HTTP response.
|
|
34
|
+
# @param callback [String] The callback service class name.
|
|
31
35
|
# @return [void]
|
|
32
36
|
def on_complete(response, callback)
|
|
33
37
|
raise NotImplementedError, "#{self.class}#on_complete must be implemented"
|
|
34
38
|
end
|
|
35
39
|
|
|
36
|
-
#
|
|
40
|
+
# Delivers an error to the callback service's `on_error` method.
|
|
37
41
|
#
|
|
38
|
-
# @param error [Error]
|
|
39
|
-
# @param callback [String] callback class name
|
|
42
|
+
# @param error [Error] The error.
|
|
43
|
+
# @param callback [String] The callback service class name.
|
|
40
44
|
# @return [void]
|
|
41
45
|
def on_error(error, callback)
|
|
42
46
|
raise NotImplementedError, "#{self.class}#on_error must be implemented"
|
|
43
47
|
end
|
|
44
48
|
|
|
45
|
-
# Re-
|
|
49
|
+
# Re-enqueues the original job, so the request runs again later.
|
|
46
50
|
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
51
|
+
# The processor calls this method for a request that didn't finish, for
|
|
52
|
+
# example when the processor shuts down.
|
|
49
53
|
#
|
|
50
|
-
# @return [String]
|
|
54
|
+
# @return [String] The new job ID.
|
|
51
55
|
def retry
|
|
52
56
|
raise NotImplementedError, "#{self.class}#retry must be implemented"
|
|
53
57
|
end
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PatientHttp
|
|
4
|
-
#
|
|
4
|
+
# Measures time with the monotonic clock, which system clock changes don't
|
|
5
|
+
# affect, and converts monotonic times to wall clock times.
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
# to system clock changes, as well as conversion between monotonic and wall clock time.
|
|
7
|
+
# @api private
|
|
8
8
|
module TimeHelper
|
|
9
9
|
extend self
|
|
10
10
|
|
|
11
|
-
#
|
|
11
|
+
# Returns the current monotonic time.
|
|
12
12
|
#
|
|
13
13
|
# Monotonic time is guaranteed to be non-decreasing and immune to system clock changes.
|
|
14
14
|
#
|
|
15
|
-
# @return [Float]
|
|
15
|
+
# @return [Float] Current monotonic time in seconds since an unspecified starting point.
|
|
16
16
|
def monotonic_time
|
|
17
17
|
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
#
|
|
20
|
+
# Converts a monotonic timestamp to wall clock time.
|
|
21
21
|
#
|
|
22
|
-
# @param monotonic_timestamp [Float]
|
|
23
|
-
# @return [Time]
|
|
22
|
+
# @param monotonic_timestamp [Float] Monotonic timestamp to convert.
|
|
23
|
+
# @return [Time] Wall clock time corresponding to the monotonic timestamp.
|
|
24
24
|
def wall_clock_time(monotonic_timestamp)
|
|
25
25
|
return nil unless monotonic_timestamp
|
|
26
26
|
|