brow 0.1.0 → 0.2.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/CHANGELOG.md +21 -3
- data/Gemfile +1 -0
- data/README.md +2 -3
- data/examples/basic.rb +3 -5
- data/lib/brow/client.rb +60 -31
- data/lib/brow/message_batch.rb +1 -1
- data/lib/brow/transport.rb +25 -24
- data/lib/brow/version.rb +1 -1
- data/lib/brow/worker.rb +0 -1
- data/lib/brow.rb +1 -4
- metadata +2 -3
- data/lib/brow/prefixed_logger.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ffdc8b811ca1be5ce149f0e21dc0c08a6f8a4f2e79368f20a69503ca33e2d997
|
4
|
+
data.tar.gz: 4f58ddc175db9c12b6fa77b13fe9f1d82a49100ea1d1cc143d335cb9a5db082b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e66c43a44fd5e10b3aba344941bbc046ed1943208da8de20bc2a96577d1642d94e3528586a1db3d7ee998c45e966bc8daa72de39933a8dfa7354a0f737c1e4b1
|
7
|
+
data.tar.gz: 704c98568783c393b58f438d23039003c816b7c363932785ceb2f747ddfca6e7da049708c7fd7e6c22f10e392b65928367bbd0d5667aea9720e7a7f7259747c2
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,23 @@
|
|
1
|
-
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
2
3
|
|
3
|
-
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
4
6
|
|
5
|
-
-
|
7
|
+
## [0.2.0] - 2021-10-25
|
8
|
+
|
9
|
+
### Changed
|
10
|
+
|
11
|
+
- [c25dce](https://github.com/jnunemaker/brow/commit/c25dcedcab2b75cfe28a561e80e537fefae6cc52) `record` is now `push`.
|
12
|
+
|
13
|
+
### Fixed
|
14
|
+
|
15
|
+
- [eceb02](https://github.com/jnunemaker/brow/commit/eceb02f810cc5ace7d7540c957fc1cf924849629) Fixed problems with shutdown (required a flush to get whatever batches were in progress) and forking (caused queue to not get worked off).
|
16
|
+
|
17
|
+
### Added
|
18
|
+
|
19
|
+
- [c7f7e4](https://github.com/jnunemaker/brow/commit/c7f7e42b0d6bfa9fa96bac58fda0ef94f93d223d) `BackoffPolicy` now gets `options` so you can pass those to `Client` and they'll make it all the way through.
|
20
|
+
|
21
|
+
## [0.1.0] - 2021-10-20
|
22
|
+
|
23
|
+
- Initial release. Let's face it I just wanted to squat on the gem name.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Brow
|
2
2
|
|
3
|
-
A generic background thread worker for shipping events via https to some API backend.
|
3
|
+
A generic background thread worker for shipping events via https to some API backend. It'll get events to your API by the sweat of its brow.
|
4
4
|
|
5
5
|
I've been wanting to build something like this for a while. This might be a terrible start. But its a start.
|
6
6
|
|
@@ -36,14 +36,13 @@ client = Brow::Client.new({
|
|
36
36
|
})
|
37
37
|
|
38
38
|
50.times do |n|
|
39
|
-
client.
|
39
|
+
client.push({
|
40
40
|
number: n,
|
41
41
|
now: Time.now.utc,
|
42
42
|
})
|
43
43
|
end
|
44
44
|
|
45
45
|
# batch of 50 events sent to api url above as json
|
46
|
-
client.flush
|
47
46
|
```
|
48
47
|
|
49
48
|
## Development
|
data/examples/basic.rb
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
require_relative "../lib/brow"
|
2
2
|
|
3
3
|
client = Brow::Client.new({
|
4
|
-
url: "https://requestbin.net/r/
|
4
|
+
url: "https://requestbin.net/r/4f09194m",
|
5
5
|
})
|
6
6
|
|
7
|
-
|
8
|
-
client.
|
7
|
+
150.times do |n|
|
8
|
+
client.push({
|
9
9
|
number: n,
|
10
10
|
now: Time.now.utc,
|
11
11
|
})
|
12
12
|
end
|
13
|
-
|
14
|
-
client.flush
|
data/lib/brow/client.rb
CHANGED
@@ -12,6 +12,9 @@ module Brow
|
|
12
12
|
# Private: Default # of items that can be in queue before we start dropping data.
|
13
13
|
MAX_QUEUE_SIZE = 10_000
|
14
14
|
|
15
|
+
# Private: Default number of seconds to wait to shutdown worker thread.
|
16
|
+
SHUTDOWN_TIMEOUT = 5
|
17
|
+
|
15
18
|
# Public: Create a new instance of a client.
|
16
19
|
#
|
17
20
|
# options - The Hash of options.
|
@@ -22,13 +25,17 @@ module Brow
|
|
22
25
|
|
23
26
|
@worker_thread = nil
|
24
27
|
@worker_mutex = Mutex.new
|
28
|
+
@pid = Process.pid
|
25
29
|
@test = options[:test]
|
26
30
|
@max_queue_size = options[:max_queue_size] || MAX_QUEUE_SIZE
|
27
31
|
@logger = options.fetch(:logger) { Brow.logger }
|
28
32
|
@queue = options.fetch(:queue) { Queue.new }
|
29
33
|
@worker = options.fetch(:worker) { Worker.new(@queue, options) }
|
34
|
+
@shutdown_timeout = options.fetch(:shutdown_timeout) { SHUTDOWN_TIMEOUT }
|
30
35
|
|
31
|
-
|
36
|
+
if options.fetch(:shutdown_automatically, true)
|
37
|
+
at_exit { shutdown }
|
38
|
+
end
|
32
39
|
end
|
33
40
|
|
34
41
|
# Public: Synchronously waits until the worker has flushed the queue.
|
@@ -37,22 +44,46 @@ module Brow
|
|
37
44
|
# specifically exit.
|
38
45
|
def flush
|
39
46
|
while !@queue.empty? || @worker.requesting?
|
40
|
-
|
47
|
+
ensure_threads_alive
|
41
48
|
sleep(0.1)
|
42
49
|
end
|
43
50
|
end
|
44
51
|
|
45
|
-
|
52
|
+
def shutdown
|
53
|
+
if @worker_thread
|
54
|
+
begin
|
55
|
+
@worker_thread.join @shutdown_timeout
|
56
|
+
rescue => error
|
57
|
+
@logger.info("[brow]") { "Error shutting down worker thread: #{error.inspect}"}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Public: Enqueues an event to eventually be transported to backend service.
|
46
63
|
#
|
47
64
|
# event - The Hash of event data.
|
48
65
|
#
|
49
66
|
# Returns Boolean of whether the item was added to the queue.
|
50
|
-
def
|
51
|
-
raise ArgumentError, "
|
67
|
+
def push(item)
|
68
|
+
raise ArgumentError, "item must be a Hash" unless item.is_a?(Hash)
|
69
|
+
|
70
|
+
item = Brow::Utils.symbolize_keys(item)
|
71
|
+
item = Brow::Utils.isoify_dates(item)
|
52
72
|
|
53
|
-
|
54
|
-
|
55
|
-
|
73
|
+
if @test
|
74
|
+
test_queue << item
|
75
|
+
return true
|
76
|
+
end
|
77
|
+
|
78
|
+
ensure_threads_alive
|
79
|
+
|
80
|
+
if @queue.length < @max_queue_size
|
81
|
+
@queue << item
|
82
|
+
true
|
83
|
+
else
|
84
|
+
@logger.warn("[brow]") { "Queue is full, dropping events. The :max_queue_size configuration parameter can be increased to prevent this from happening." }
|
85
|
+
false
|
86
|
+
end
|
56
87
|
end
|
57
88
|
|
58
89
|
# Public: Returns the number of messages in the queue.
|
@@ -61,7 +92,7 @@ module Brow
|
|
61
92
|
end
|
62
93
|
|
63
94
|
# Public: For test purposes only. If test: true is passed to #initialize
|
64
|
-
# then all
|
95
|
+
# then all pushing of events will go to test queue in memory so they can
|
65
96
|
# be verified with assertions.
|
66
97
|
def test_queue
|
67
98
|
unless @test
|
@@ -73,36 +104,34 @@ module Brow
|
|
73
104
|
|
74
105
|
private
|
75
106
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
def enqueue(action)
|
80
|
-
if @test
|
81
|
-
test_queue << action
|
82
|
-
return true
|
83
|
-
end
|
84
|
-
|
85
|
-
if @queue.length < @max_queue_size
|
86
|
-
@queue << action
|
87
|
-
ensure_worker_running
|
107
|
+
def forked?
|
108
|
+
@pid != Process.pid
|
109
|
+
end
|
88
110
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
false
|
93
|
-
end
|
111
|
+
def ensure_threads_alive
|
112
|
+
reset if forked?
|
113
|
+
ensure_worker_running
|
94
114
|
end
|
95
115
|
|
96
116
|
def ensure_worker_running
|
97
|
-
return
|
98
|
-
|
117
|
+
# If another thread is starting worker thread, then return early so this
|
118
|
+
# thread can enqueue and move on with life.
|
119
|
+
return unless @worker_mutex.try_lock
|
120
|
+
|
121
|
+
begin
|
99
122
|
return if worker_running?
|
100
|
-
@worker_thread = Thread.new
|
101
|
-
|
102
|
-
|
123
|
+
@worker_thread = Thread.new { @worker.run }
|
124
|
+
ensure
|
125
|
+
@worker_mutex.unlock
|
103
126
|
end
|
104
127
|
end
|
105
128
|
|
129
|
+
def reset
|
130
|
+
@pid = Process.pid
|
131
|
+
@worker_mutex.unlock if @worker_mutex.locked?
|
132
|
+
@queue.clear
|
133
|
+
end
|
134
|
+
|
106
135
|
def worker_running?
|
107
136
|
@worker_thread && @worker_thread.alive?
|
108
137
|
end
|
data/lib/brow/message_batch.rb
CHANGED
@@ -41,7 +41,7 @@ module Brow
|
|
41
41
|
message_json_size = message_json.bytesize
|
42
42
|
|
43
43
|
if message_too_big?(message_json_size)
|
44
|
-
@logger.error('a message exceeded the maximum allowed size'
|
44
|
+
@logger.error("[brow]") { 'a message exceeded the maximum allowed size' }
|
45
45
|
else
|
46
46
|
@messages << message
|
47
47
|
@json_size += message_json_size + 1 # One byte for the comma
|
data/lib/brow/transport.rb
CHANGED
@@ -10,6 +10,8 @@ require_relative 'backoff_policy'
|
|
10
10
|
module Brow
|
11
11
|
class Transport
|
12
12
|
RETRIES = 10
|
13
|
+
READ_TIMEOUT = 8
|
14
|
+
OPEN_TIMEOUT = 4
|
13
15
|
HEADERS = {
|
14
16
|
"Accept" => "application/json",
|
15
17
|
"Content-Type" => "application/json",
|
@@ -18,8 +20,6 @@ module Brow
|
|
18
20
|
"Client-Language-Version" => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
|
19
21
|
"Client-Platform" => RUBY_PLATFORM,
|
20
22
|
"Client-Engine" => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
|
21
|
-
"Client-Pid" => Process.pid.to_s,
|
22
|
-
"Client-Thread" => Thread.current.object_id.to_s,
|
23
23
|
"Client-Hostname" => Socket.gethostname,
|
24
24
|
}
|
25
25
|
|
@@ -39,32 +39,29 @@ module Brow
|
|
39
39
|
|
40
40
|
@logger = options.fetch(:logger) { Brow.logger }
|
41
41
|
@backoff_policy = options.fetch(:backoff_policy) {
|
42
|
-
Brow::BackoffPolicy.new
|
42
|
+
Brow::BackoffPolicy.new(options)
|
43
43
|
}
|
44
44
|
|
45
45
|
@http = Net::HTTP.new(@uri.host, @uri.port)
|
46
46
|
@http.use_ssl = @uri.scheme == "https"
|
47
|
-
@http.read_timeout = options[:read_timeout] ||
|
48
|
-
@http.open_timeout = options[:open_timeout] ||
|
47
|
+
@http.read_timeout = options[:read_timeout] || READ_TIMEOUT
|
48
|
+
@http.open_timeout = options[:open_timeout] || OPEN_TIMEOUT
|
49
49
|
end
|
50
50
|
|
51
51
|
# Sends a batch of messages to the API
|
52
52
|
#
|
53
53
|
# @return [Response] API response
|
54
54
|
def send_batch(batch)
|
55
|
-
@logger.debug("Sending request for #{batch.length} items"
|
55
|
+
@logger.debug("[brow]") { "Sending request for #{batch.length} items" }
|
56
56
|
|
57
57
|
last_response, exception = retry_with_backoff(@retries) do
|
58
58
|
response = send_request(batch)
|
59
|
-
|
60
|
-
|
61
|
-
@logger.debug("Response status code: #{status_code}")
|
62
|
-
|
63
|
-
[Response.new(status_code, nil), should_retry]
|
59
|
+
@logger.debug("[brow]") { "Response: status=#{response.code}, body=#{response.body}" }
|
60
|
+
[Response.new(response.code.to_i, nil), retry?(response)]
|
64
61
|
end
|
65
62
|
|
66
63
|
if exception
|
67
|
-
@logger.error(exception.message
|
64
|
+
@logger.error("[brow]") { exception.message }
|
68
65
|
exception.backtrace.each { |line| @logger.error(line) }
|
69
66
|
Response.new(-1, exception.to_s)
|
70
67
|
else
|
@@ -79,18 +76,19 @@ module Brow
|
|
79
76
|
|
80
77
|
private
|
81
78
|
|
82
|
-
def
|
79
|
+
def retry?(response)
|
80
|
+
status_code = response.code.to_i
|
83
81
|
if status_code >= 500
|
84
82
|
# Server error. Retry and log.
|
85
|
-
@logger.info("Server error: status=#{status_code}, body=#{body}"
|
83
|
+
@logger.info("[brow]") { "Server error: status=#{status_code}, body=#{response.body}" }
|
86
84
|
true
|
87
85
|
elsif status_code == 429
|
88
|
-
# Rate limited
|
89
|
-
@logger.info "Rate limit error"
|
86
|
+
# Rate limited. Retry and log.
|
87
|
+
@logger.info("[brow]") { "Rate limit error: body=#{response.body}" }
|
90
88
|
true
|
91
89
|
elsif status_code >= 400
|
92
90
|
# Client error. Do not retry, but log.
|
93
|
-
@logger.error("Client error: status=#{status_code}, body=#{body}"
|
91
|
+
@logger.error("[brow]") { "Client error: status=#{status_code}, body=#{response.body}" }
|
94
92
|
false
|
95
93
|
else
|
96
94
|
false
|
@@ -112,13 +110,13 @@ module Brow
|
|
112
110
|
result, should_retry = yield
|
113
111
|
return [result, nil] unless should_retry
|
114
112
|
rescue StandardError => error
|
115
|
-
@logger.debug "Request error: #{error}"
|
113
|
+
@logger.debug("[brow]") { "Request error: #{error}" }
|
116
114
|
should_retry = true
|
117
115
|
caught_exception = error
|
118
116
|
end
|
119
117
|
|
120
118
|
if should_retry && (retries_remaining > 1)
|
121
|
-
@logger.debug("Retrying request, #{retries_remaining} retries left"
|
119
|
+
@logger.debug("[brow]") { "Retrying request, #{retries_remaining} retries left" }
|
122
120
|
sleep(@backoff_policy.next_interval.to_f / 1000)
|
123
121
|
retry_with_backoff(retries_remaining - 1, &block)
|
124
122
|
else
|
@@ -126,12 +124,15 @@ module Brow
|
|
126
124
|
end
|
127
125
|
end
|
128
126
|
|
129
|
-
# Sends a request for the batch, returns [status_code, body]
|
130
127
|
def send_request(batch)
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
128
|
+
headers = {
|
129
|
+
"Client-Pid" => Process.pid.to_s,
|
130
|
+
"Client-Thread" => Thread.current.object_id.to_s,
|
131
|
+
}.merge(@headers)
|
132
|
+
|
133
|
+
@http.start unless @http.started?
|
134
|
+
request = Net::HTTP::Post.new(@uri.path, headers)
|
135
|
+
@http.request(request, batch.to_json)
|
135
136
|
end
|
136
137
|
end
|
137
138
|
end
|
data/lib/brow/version.rb
CHANGED
data/lib/brow/worker.rb
CHANGED
@@ -28,7 +28,6 @@ module Brow
|
|
28
28
|
options = Brow::Utils.symbolize_keys(options)
|
29
29
|
@on_error = options[:on_error] || DEFAULT_ON_ERROR
|
30
30
|
@transport = options.fetch(:transport) { Transport.new(options) }
|
31
|
-
@logger = options.fetch(:logger) { Brow.logger }
|
32
31
|
@batch = options.fetch(:batch) { MessageBatch.new(max_size: options[:batch_size]) }
|
33
32
|
end
|
34
33
|
|
data/lib/brow.rb
CHANGED
@@ -10,13 +10,11 @@ module Brow
|
|
10
10
|
def self.logger
|
11
11
|
return @logger if @logger
|
12
12
|
|
13
|
-
|
13
|
+
@logger = if defined?(Rails)
|
14
14
|
Rails.logger
|
15
15
|
else
|
16
16
|
Logger.new(STDOUT)
|
17
17
|
end
|
18
|
-
|
19
|
-
@logger = PrefixedLogger.new(base_logger, "[brow]")
|
20
18
|
end
|
21
19
|
|
22
20
|
# Public: Sets the logger instance to use for logging things.
|
@@ -26,4 +24,3 @@ module Brow
|
|
26
24
|
end
|
27
25
|
|
28
26
|
require_relative "brow/client"
|
29
|
-
require_relative "brow/prefixed_logger"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: brow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- John Nunemaker
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-10-
|
11
|
+
date: 2021-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -30,7 +30,6 @@ files:
|
|
30
30
|
- lib/brow/backoff_policy.rb
|
31
31
|
- lib/brow/client.rb
|
32
32
|
- lib/brow/message_batch.rb
|
33
|
-
- lib/brow/prefixed_logger.rb
|
34
33
|
- lib/brow/response.rb
|
35
34
|
- lib/brow/test_queue.rb
|
36
35
|
- lib/brow/transport.rb
|
data/lib/brow/prefixed_logger.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
module Brow
|
2
|
-
# Internal: Wraps an existing logger and adds a prefix to all messages.
|
3
|
-
class PrefixedLogger
|
4
|
-
def initialize(logger, prefix)
|
5
|
-
@logger = logger
|
6
|
-
@prefix = prefix
|
7
|
-
end
|
8
|
-
|
9
|
-
def debug(message)
|
10
|
-
@logger.debug("#{@prefix} #{message}")
|
11
|
-
end
|
12
|
-
|
13
|
-
def info(message)
|
14
|
-
@logger.info("#{@prefix} #{message}")
|
15
|
-
end
|
16
|
-
|
17
|
-
def warn(message)
|
18
|
-
@logger.warn("#{@prefix} #{message}")
|
19
|
-
end
|
20
|
-
|
21
|
-
def error(message)
|
22
|
-
@logger.error("#{@prefix} #{message}")
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|