httpx 0.16.1 → 0.18.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/doc/release_notes/0_17_0.md +49 -0
  4. data/doc/release_notes/0_18_0.md +69 -0
  5. data/doc/release_notes/0_18_1.md +12 -0
  6. data/doc/release_notes/0_18_2.md +10 -0
  7. data/lib/httpx/adapters/datadog.rb +1 -1
  8. data/lib/httpx/adapters/faraday.rb +5 -3
  9. data/lib/httpx/adapters/webmock.rb +9 -3
  10. data/lib/httpx/altsvc.rb +2 -2
  11. data/lib/httpx/chainable.rb +4 -4
  12. data/lib/httpx/connection/http1.rb +23 -14
  13. data/lib/httpx/connection/http2.rb +35 -17
  14. data/lib/httpx/connection.rb +74 -76
  15. data/lib/httpx/domain_name.rb +1 -1
  16. data/lib/httpx/extensions.rb +50 -4
  17. data/lib/httpx/headers.rb +1 -1
  18. data/lib/httpx/io/ssl.rb +5 -1
  19. data/lib/httpx/io/tls.rb +7 -7
  20. data/lib/httpx/loggable.rb +5 -5
  21. data/lib/httpx/options.rb +35 -13
  22. data/lib/httpx/parser/http1.rb +10 -6
  23. data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
  24. data/lib/httpx/plugins/aws_sigv4.rb +9 -11
  25. data/lib/httpx/plugins/compression.rb +5 -3
  26. data/lib/httpx/plugins/cookies/jar.rb +1 -1
  27. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  28. data/lib/httpx/plugins/expect.rb +7 -3
  29. data/lib/httpx/plugins/grpc/message.rb +2 -2
  30. data/lib/httpx/plugins/grpc.rb +3 -3
  31. data/lib/httpx/plugins/h2c.rb +7 -3
  32. data/lib/httpx/plugins/internal_telemetry.rb +8 -8
  33. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  34. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  35. data/lib/httpx/plugins/multipart/part.rb +2 -2
  36. data/lib/httpx/plugins/multipart.rb +16 -2
  37. data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
  38. data/lib/httpx/plugins/proxy/ssh.rb +11 -4
  39. data/lib/httpx/plugins/proxy.rb +6 -4
  40. data/lib/httpx/plugins/response_cache/store.rb +55 -0
  41. data/lib/httpx/plugins/response_cache.rb +88 -0
  42. data/lib/httpx/plugins/retries.rb +36 -14
  43. data/lib/httpx/plugins/stream.rb +3 -4
  44. data/lib/httpx/pool.rb +39 -13
  45. data/lib/httpx/registry.rb +1 -1
  46. data/lib/httpx/request.rb +12 -13
  47. data/lib/httpx/resolver/https.rb +5 -7
  48. data/lib/httpx/resolver/native.rb +4 -2
  49. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  50. data/lib/httpx/resolver/system.rb +2 -0
  51. data/lib/httpx/resolver.rb +2 -2
  52. data/lib/httpx/response.rb +60 -44
  53. data/lib/httpx/selector.rb +16 -19
  54. data/lib/httpx/session.rb +22 -15
  55. data/lib/httpx/session2.rb +1 -1
  56. data/lib/httpx/timers.rb +84 -0
  57. data/lib/httpx/transcoder/body.rb +2 -1
  58. data/lib/httpx/transcoder/form.rb +20 -0
  59. data/lib/httpx/transcoder/json.rb +12 -0
  60. data/lib/httpx/transcoder.rb +62 -1
  61. data/lib/httpx/utils.rb +10 -2
  62. data/lib/httpx/version.rb +1 -1
  63. data/lib/httpx.rb +1 -0
  64. data/sig/buffer.rbs +2 -2
  65. data/sig/chainable.rbs +7 -1
  66. data/sig/connection/http1.rbs +15 -4
  67. data/sig/connection/http2.rbs +19 -5
  68. data/sig/connection.rbs +15 -9
  69. data/sig/headers.rbs +19 -18
  70. data/sig/options.rbs +13 -5
  71. data/sig/parser/http1.rbs +3 -3
  72. data/sig/plugins/aws_sdk_authentication.rbs +22 -4
  73. data/sig/plugins/aws_sigv4.rbs +12 -3
  74. data/sig/plugins/basic_authentication.rbs +1 -1
  75. data/sig/plugins/multipart.rbs +64 -8
  76. data/sig/plugins/proxy.rbs +6 -6
  77. data/sig/plugins/response_cache.rbs +35 -0
  78. data/sig/plugins/retries.rbs +3 -0
  79. data/sig/pool.rbs +6 -0
  80. data/sig/request.rbs +11 -8
  81. data/sig/resolver/native.rbs +2 -1
  82. data/sig/resolver/resolver_mixin.rbs +1 -1
  83. data/sig/resolver/system.rbs +3 -1
  84. data/sig/response.rbs +11 -4
  85. data/sig/selector.rbs +8 -6
  86. data/sig/session.rbs +8 -14
  87. data/sig/timers.rbs +32 -0
  88. data/sig/transcoder/form.rbs +1 -0
  89. data/sig/transcoder/json.rbs +1 -0
  90. data/sig/transcoder.rbs +5 -4
  91. data/sig/utils.rbs +4 -0
  92. metadata +18 -17
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds support for retrying requests when certain errors happen.
7
+ #
8
+ # https://gitlab.com/honeyryderchuck/httpx/wikis/Response-Cache
9
+ #
10
+ module ResponseCache
11
+ CACHEABLE_VERBS = %i[get head].freeze
12
+ private_constant :CACHEABLE_VERBS
13
+
14
+ class << self
15
+ def load_dependencies(*)
16
+ require_relative "response_cache/store"
17
+ end
18
+
19
+ def cacheable_request?(request)
20
+ CACHEABLE_VERBS.include?(request.verb)
21
+ end
22
+
23
+ def cacheable_response?(response)
24
+ response.is_a?(Response) &&
25
+ # partial responses shall not be cached, only full ones.
26
+ response.status != 206 && (
27
+ response.headers.key?("etag") || response.headers.key?("last-modified-at")
28
+ )
29
+ end
30
+
31
+ def cached_response?(response)
32
+ response.is_a?(Response) && response.status == 304
33
+ end
34
+
35
+ def extra_options(options)
36
+ options.merge(response_cache_store: Store.new)
37
+ end
38
+ end
39
+
40
+ module OptionsMethods
41
+ def option_response_cache_store(value)
42
+ raise TypeError, "must be an instance of #{Store}" unless value.is_a?(Store)
43
+
44
+ value
45
+ end
46
+ end
47
+
48
+ module InstanceMethods
49
+ def clear_response_cache
50
+ @options.response_cache_store.clear
51
+ end
52
+
53
+ def build_request(*)
54
+ request = super
55
+ return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request.uri)
56
+
57
+ @options.response_cache_store.prepare(request)
58
+
59
+ request
60
+ end
61
+
62
+ def fetch_response(request, *)
63
+ response = super
64
+
65
+ if response && ResponseCache.cached_response?(response)
66
+ log { "returning cached response for #{request.uri}" }
67
+ cached_response = @options.response_cache_store.lookup(request.uri)
68
+
69
+ response.copy_from_cached(cached_response)
70
+ end
71
+
72
+ @options.response_cache_store.cache(request.uri, response) if response && ResponseCache.cacheable_response?(response)
73
+
74
+ response
75
+ end
76
+ end
77
+
78
+ module ResponseMethods
79
+ def copy_from_cached(other)
80
+ @body = other.body
81
+
82
+ @body.__send__(:rewind)
83
+ end
84
+ end
85
+ end
86
+ register_plugin :response_cache, ResponseCache
87
+ end
88
+ end
@@ -12,19 +12,29 @@ module HTTPX
12
12
  # TODO: pass max_retries in a configure/load block
13
13
 
14
14
  IDEMPOTENT_METHODS = %i[get options head put delete].freeze
15
- RETRYABLE_ERRORS = [IOError,
16
- EOFError,
17
- Errno::ECONNRESET,
18
- Errno::ECONNABORTED,
19
- Errno::EPIPE,
20
- TLSError,
21
- TimeoutError,
22
- Parser::Error,
23
- Errno::EINVAL,
24
- Errno::ETIMEDOUT].freeze
25
-
26
- def self.extra_options(options)
27
- options.merge(max_retries: MAX_RETRIES)
15
+ RETRYABLE_ERRORS = [
16
+ IOError,
17
+ EOFError,
18
+ Errno::ECONNRESET,
19
+ Errno::ECONNABORTED,
20
+ Errno::EPIPE,
21
+ Errno::EINVAL,
22
+ Errno::ETIMEDOUT,
23
+ Parser::Error,
24
+ TLSError,
25
+ TimeoutError,
26
+ Connection::HTTP2::GoawayError,
27
+ ].freeze
28
+ DEFAULT_JITTER = ->(interval) { interval * (0.5 * (1 + rand)) }
29
+
30
+ if ENV.key?("HTTPX_NO_JITTER")
31
+ def self.extra_options(options)
32
+ options.merge(max_retries: MAX_RETRIES)
33
+ end
34
+ else
35
+ def self.extra_options(options)
36
+ options.merge(max_retries: MAX_RETRIES, retry_jitter: DEFAULT_JITTER)
37
+ end
28
38
  end
29
39
 
30
40
  module OptionsMethods
@@ -38,6 +48,13 @@ module HTTPX
38
48
  value
39
49
  end
40
50
 
51
+ def option_retry_jitter(value)
52
+ # return early if callable
53
+ raise TypeError, ":retry_jitter must be callable" unless value.respond_to?(:call)
54
+
55
+ value
56
+ end
57
+
41
58
  def option_max_retries(value)
42
59
  num = Integer(value)
43
60
  raise TypeError, ":max_retries must be positive" unless num.positive?
@@ -87,10 +104,15 @@ module HTTPX
87
104
  retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
88
105
 
89
106
  if retry_after
107
+ # apply jitter
108
+ if (jitter = request.options.retry_jitter)
109
+ retry_after = jitter.call(retry_after)
110
+ end
90
111
 
112
+ retry_start = Utils.now
91
113
  log { "retrying after #{retry_after} secs..." }
92
114
  pool.after(retry_after) do
93
- log { "retrying!!" }
115
+ log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
94
116
  connection = find_connection(request, connections, options)
95
117
  connection.send(request)
96
118
  end
@@ -6,11 +6,10 @@ module HTTPX
6
6
  @request = request
7
7
  @session = session
8
8
  @connections = connections
9
- @options = @request.options
10
9
  end
11
10
 
12
11
  def each(&block)
13
- return enum_for(__method__) unless block_given?
12
+ return enum_for(__method__) unless block
14
13
 
15
14
  raise Error, "response already streamed" if @response
16
15
 
@@ -72,7 +71,7 @@ module HTTPX
72
71
  private
73
72
 
74
73
  def response
75
- @session.__send__(:receive_requests, [@request], @connections, @options) until @request.response
74
+ @session.__send__(:receive_requests, [@request], @connections) until @request.response
76
75
 
77
76
  @request.response
78
77
  end
@@ -106,7 +105,7 @@ module HTTPX
106
105
 
107
106
  request = requests.first
108
107
 
109
- connections = _send_requests(requests, request.options)
108
+ connections = _send_requests(requests)
110
109
 
111
110
  StreamResponse.new(request, self, connections)
112
111
  end
data/lib/httpx/pool.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
- require "timers"
5
4
  require "httpx/selector"
6
5
  require "httpx/connection"
7
6
  require "httpx/resolver"
8
7
 
9
8
  module HTTPX
10
9
  class Pool
10
+ using ArrayExtensions
11
11
  extend Forwardable
12
12
 
13
13
  def_delegator :@timers, :after
@@ -15,7 +15,7 @@ module HTTPX
15
15
  def initialize
16
16
  @resolvers = {}
17
17
  @_resolver_ios = {}
18
- @timers = Timers::Group.new
18
+ @timers = Timers.new
19
19
  @selector = Selector.new
20
20
  @connections = []
21
21
  @connected_connections = 0
@@ -27,15 +27,18 @@ module HTTPX
27
27
 
28
28
  def next_tick
29
29
  catch(:jump_tick) do
30
- timeout = [next_timeout, @timers.wait_interval].compact.min
30
+ timeout = next_timeout
31
31
  if timeout && timeout.negative?
32
32
  @timers.fire
33
33
  throw(:jump_tick)
34
34
  end
35
35
 
36
- @selector.select(timeout, &:call)
37
-
38
- @timers.fire
36
+ begin
37
+ @selector.select(timeout, &:call)
38
+ @timers.fire
39
+ rescue TimeoutError => e
40
+ @timers.fire(e)
41
+ end
39
42
  end
40
43
  rescue StandardError => e
41
44
  @connections.each do |connection|
@@ -64,6 +67,16 @@ module HTTPX
64
67
  connection.on(:open) do
65
68
  @connected_connections += 1
66
69
  end
70
+ connection.on(:activate) do
71
+ select_connection(connection)
72
+ end
73
+ end
74
+
75
+ def deactivate(connections)
76
+ connections.each do |connection|
77
+ connection.deactivate
78
+ deselect_connection(connection) if connection.state == :inactive
79
+ end
67
80
  end
68
81
 
69
82
  # opens a connection to the IP reachable through +uri+.
@@ -81,7 +94,7 @@ module HTTPX
81
94
  def resolve_connection(connection)
82
95
  @connections << connection unless @connections.include?(connection)
83
96
 
84
- if connection.addresses || connection.state == :open
97
+ if connection.addresses || connection.open?
85
98
  #
86
99
  # there are two cases in which we want to activate initialization of
87
100
  # connection immediately:
@@ -98,7 +111,7 @@ module HTTPX
98
111
  resolver << connection
99
112
  return if resolver.empty?
100
113
 
101
- @_resolver_ios[resolver] ||= @selector.register(resolver)
114
+ @_resolver_ios[resolver] ||= select_connection(resolver)
102
115
  end
103
116
 
104
117
  def on_resolver_connection(connection)
@@ -107,7 +120,7 @@ module HTTPX
107
120
  end
108
121
  return register_connection(connection) unless found_connection
109
122
 
110
- if found_connection.state == :open
123
+ if found_connection.open?
111
124
  coalesce_connections(found_connection, connection)
112
125
  throw(:coalesced, found_connection)
113
126
  else
@@ -129,7 +142,7 @@ module HTTPX
129
142
 
130
143
  @resolvers.delete(resolver_type)
131
144
 
132
- @selector.deregister(resolver)
145
+ deselect_connection(resolver)
133
146
  @_resolver_ios.delete(resolver)
134
147
  resolver.close unless resolver.closed?
135
148
  end
@@ -140,7 +153,7 @@ module HTTPX
140
153
  # consider it connected already.
141
154
  @connected_connections += 1
142
155
  end
143
- @selector.register(connection)
156
+ select_connection(connection)
144
157
  connection.on(:close) do
145
158
  unregister_connection(connection)
146
159
  end
@@ -148,10 +161,18 @@ module HTTPX
148
161
 
149
162
  def unregister_connection(connection)
150
163
  @connections.delete(connection)
151
- @selector.deregister(connection)
164
+ deselect_connection(connection)
152
165
  @connected_connections -= 1
153
166
  end
154
167
 
168
+ def select_connection(connection)
169
+ @selector.register(connection)
170
+ end
171
+
172
+ def deselect_connection(connection)
173
+ @selector.deregister(connection)
174
+ end
175
+
155
176
  def coalesce_connections(conn1, conn2)
156
177
  if conn1.coalescable?(conn2)
157
178
  conn1.merge(conn2)
@@ -162,7 +183,11 @@ module HTTPX
162
183
  end
163
184
 
164
185
  def next_timeout
165
- @resolvers.values.reject(&:closed?).map(&:timeout).compact.min || @connections.map(&:timeout).compact.min
186
+ [
187
+ @timers.wait_interval,
188
+ *@resolvers.values.reject(&:closed?).filter_map(&:timeout),
189
+ *@connections.filter_map(&:timeout),
190
+ ].compact.min
166
191
  end
167
192
 
168
193
  def find_resolver_for(connection)
@@ -172,6 +197,7 @@ module HTTPX
172
197
 
173
198
  @resolvers[resolver_type] ||= begin
174
199
  resolver = resolver_type.new(connection_options)
200
+ resolver.pool = self if resolver.respond_to?(:pool=)
175
201
  resolver.on(:resolve, &method(:on_resolver_connection))
176
202
  resolver.on(:error, &method(:on_resolver_error))
177
203
  resolver.on(:close) { on_resolver_close(resolver) }
@@ -59,7 +59,7 @@ module HTTPX
59
59
  @registry ||= {}
60
60
  return @registry if tag.nil?
61
61
 
62
- handler = @registry.fetch(tag)
62
+ handler = @registry[tag]
63
63
  raise(Error, "#{tag} is not registered in #{self}") unless handler
64
64
 
65
65
  handler
data/lib/httpx/request.rb CHANGED
@@ -41,16 +41,15 @@ module HTTPX
41
41
 
42
42
  def_delegator :@body, :empty?
43
43
 
44
- def_delegator :@body, :chunk!
45
-
46
44
  def initialize(verb, uri, options = {})
47
45
  @verb = verb.to_s.downcase.to_sym
48
46
  @options = Options.new(options)
49
47
  @uri = Utils.to_uri(uri)
50
48
  if @uri.relative?
51
- raise(Error, "invalid URI: #{@uri}") unless @options.origin
49
+ origin = @options.origin
50
+ raise(Error, "invalid URI: #{@uri}") unless origin
52
51
 
53
- @uri = @options.origin.merge(@uri)
52
+ @uri = origin.merge(@uri)
54
53
  end
55
54
 
56
55
  raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
@@ -98,7 +97,7 @@ module HTTPX
98
97
  def response=(response)
99
98
  return unless response
100
99
 
101
- if response.status == 100
100
+ if response.is_a?(Response) && response.status == 100
102
101
  @informational_status = response.status
103
102
  return
104
103
  end
@@ -149,16 +148,16 @@ module HTTPX
149
148
  # :nocov:
150
149
  def inspect
151
150
  "#<HTTPX::Request:#{object_id} " \
152
- "#{@verb.to_s.upcase} " \
153
- "#{uri} " \
154
- "@headers=#{@headers} " \
155
- "@body=#{@body}>"
151
+ "#{@verb.to_s.upcase} " \
152
+ "#{uri} " \
153
+ "@headers=#{@headers} " \
154
+ "@body=#{@body}>"
156
155
  end
157
156
  # :nocov:
158
157
 
159
158
  class Body < SimpleDelegator
160
159
  class << self
161
- def new(*, options)
160
+ def new(_, options)
162
161
  return options.body if options.body.is_a?(self)
163
162
 
164
163
  super
@@ -182,7 +181,7 @@ module HTTPX
182
181
  end
183
182
 
184
183
  def each(&block)
185
- return enum_for(__method__) unless block_given?
184
+ return enum_for(__method__) unless block
186
185
  return if @body.nil?
187
186
 
188
187
  body = stream(@body)
@@ -223,7 +222,7 @@ module HTTPX
223
222
  def unbounded_body?
224
223
  return @unbounded_body if defined?(@unbounded_body)
225
224
 
226
- @unbounded_body = (chunked? || @body.bytesize == Float::INFINITY)
225
+ @unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
227
226
  end
228
227
 
229
228
  def chunked?
@@ -237,7 +236,7 @@ module HTTPX
237
236
  # :nocov:
238
237
  def inspect
239
238
  "#<HTTPX::Request::Body:#{object_id} " \
240
- "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
239
+ "#{unbounded_body? ? "stream" : "@bytesize=#{bytesize}"}>"
241
240
  end
242
241
  # :nocov:
243
242
  end
@@ -24,7 +24,9 @@ module HTTPX
24
24
  record_types: RECORD_TYPES.keys,
25
25
  }.freeze
26
26
 
27
- def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close
27
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
28
+
29
+ attr_writer :pool
28
30
 
29
31
  def initialize(options)
30
32
  @options = Options.new(options)
@@ -63,15 +65,11 @@ module HTTPX
63
65
 
64
66
  private
65
67
 
66
- def pool
67
- Thread.current[:httpx_connection_pool] ||= Pool.new
68
- end
69
-
70
68
  def resolver_connection
71
- @resolver_connection ||= pool.find_connection(@uri, @options) || begin
69
+ @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
72
70
  @building_connection = true
73
71
  connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
74
- pool.init_connection(connection, @options)
72
+ @pool.init_connection(connection, @options)
75
73
  emit_addresses(connection, @uri_addresses)
76
74
  @building_connection = false
77
75
  connection
@@ -47,6 +47,8 @@ module HTTPX
47
47
 
48
48
  def_delegator :@connections, :empty?
49
49
 
50
+ attr_reader :state
51
+
50
52
  def initialize(options)
51
53
  @options = Options.new(options)
52
54
  @ns_index = 0
@@ -120,7 +122,7 @@ module HTTPX
120
122
  def timeout
121
123
  return if @connections.empty?
122
124
 
123
- @start_timeout = Process.clock_gettime(Process::CLOCK_MONOTONIC)
125
+ @start_timeout = Utils.now
124
126
  hosts = @queries.keys
125
127
  @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
126
128
  end
@@ -140,7 +142,7 @@ module HTTPX
140
142
  def do_retry
141
143
  return if @queries.empty?
142
144
 
143
- loop_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_timeout
145
+ loop_time = Utils.elapsed_time(@start_timeout)
144
146
  connections = []
145
147
  queries = {}
146
148
  while (query = @queries.shift)
@@ -9,7 +9,7 @@ module HTTPX
9
9
  include Callbacks
10
10
  include Loggable
11
11
 
12
- CHECK_IF_IP = proc do |name|
12
+ CHECK_IF_IP = lambda do |name|
13
13
  begin
14
14
  IPAddr.new(name)
15
15
  true
@@ -55,6 +55,7 @@ module HTTPX
55
55
  return if ips.empty?
56
56
 
57
57
  ips.map { |ip| IPAddr.new(ip) }
58
+ rescue IOError
58
59
  end
59
60
 
60
61
  def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
@@ -12,6 +12,8 @@ module HTTPX
12
12
  Resolv::DNS::EncodeError,
13
13
  Resolv::DNS::DecodeError].freeze
14
14
 
15
+ attr_reader :state
16
+
15
17
  def initialize(options)
16
18
  @options = Options.new(options)
17
19
  @resolver_options = @options.resolver_options
@@ -26,14 +26,14 @@ module HTTPX
26
26
  module_function
27
27
 
28
28
  def cached_lookup(hostname)
29
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ now = Utils.now
30
30
  @lookup_mutex.synchronize do
31
31
  lookup(hostname, now)
32
32
  end
33
33
  end
34
34
 
35
35
  def cached_lookup_set(hostname, entries)
36
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
+ now = Utils.now
37
37
  entries.each do |entry|
38
38
  entry["TTL"] += now
39
39
  end