brow 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94b1026b806df0a58a01aa0bc911ca756251dc5d0de510ae1c12d42665b00559
4
- data.tar.gz: 55b2b5454086ac78e22681ced81bb6d500dd9c6537ebfcf67bbc9a445a2d9a79
3
+ metadata.gz: 6b51fe73e4a857aa33f3546cf5d7f0e434f467c19e585e00e8b7a5535765d0b4
4
+ data.tar.gz: b36b39d1293d6b32940b5f9306c7a679c6438cc76377ea376560223343c3e6e3
5
5
  SHA512:
6
- metadata.gz: 2d23c1ff291ca611a65b1357392e2f4275502fc3467a629e85ff631f7585050095ef4cc86f0b86981fb0e7e790ce709bd2a0e2b511f0e706954cec6221ae40db
7
- data.tar.gz: 884ffd8cebbcaf2681a1347a1115892676c86de12dcef693e5364531e37cef0d3700343cb8e4d2393270003c541d9efbd0e1c427fef904b66f475eb81ece2246
6
+ metadata.gz: 7bea156ce56ed4bc49688db5dfd9ce810fcbc6e6a18513445dc734792374b0831063e7ac831c9a1eed4f50459d95cef38181afa3071e5336e1f671a576844938
7
+ data.tar.gz: ad5cc322c41f678f7de7df6add329defc9c90789fbe2a71df62156d58500a3b65648dacf0bc5bdeb0d986a5d2f542cccceb6e85dd55ef9b679c23a18f81ffc90
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2021-11-04
9
+
10
+ ### Added
11
+
12
+ - Allow configuring most options from ENV variables by default (fb7819b0237a81e573677f3050446a4f41e8fb47).
13
+ - Extra early return to avoid mutex lock if thread is alive (ac7dcfe54ee83b18e0df5ab3778a077584c843bd).
14
+ - Validation on many of the configuration options (c50b11a2917272a87937f8aa86007816a87c63a2 and 07e2581397f870249a347d4d68e4fce172d33cef).
15
+
16
+ ### Changed
17
+
18
+ - Stop stringifying keys. Just enqueue whatever is passed and let JSON do the rest (2e63d5328e048f0fad9fc41ca0935f97fb5ada2f).
19
+ - A bunch of test stuff to make them faster and less flaky.
20
+
8
21
  ## [0.3.0] - 2021-10-29
9
22
 
10
23
  https://github.com/jnunemaker/brow/pull/4
data/Gemfile CHANGED
@@ -8,7 +8,7 @@ gem "minitest", "~> 5.0"
8
8
  gem "maxitest", "~> 4.1"
9
9
  gem "minitest-heat", "~> 0.0"
10
10
  gem "webmock", "~> 3.10.0"
11
- gem "rack", "~> 2.2.3"
11
+ gem "climate_control", "~> 0.2.0"
12
12
 
13
13
  group(:guard) do
14
14
  gem "guard", "~> 2.18.0"
data/Guardfile CHANGED
@@ -32,6 +32,7 @@ guard :minitest do
32
32
  watch(%r{^test/(.*)\/?(.*)_test\.rb$})
33
33
  watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
34
34
  watch(%r{^test/test_helper\.rb$}) { 'test' }
35
+ watch(%r{^test/support/fake_server\.rb$}) { 'test' }
35
36
 
36
37
  # with Minitest::Spec
37
38
  # watch(%r{^spec/(.*)_spec\.rb$})
data/examples/basic.rb CHANGED
@@ -1,20 +1,14 @@
1
1
  require_relative "../lib/brow"
2
+ require_relative "echo_server"
2
3
 
3
4
  client = Brow::Client.new({
4
- url: "https://requestbin.net/r/2bp3p3vn",
5
+ url: "http://localhost:#{EchoServer.instance.port}",
5
6
  batch_size: 10,
6
7
  })
7
8
 
8
9
  5.times do |n|
9
- client.push(n: n, parent: true)
10
+ client.push({
11
+ n: n,
12
+ now: Time.now.utc,
13
+ })
10
14
  end
11
-
12
- pid = fork {
13
- 15.times do |n|
14
- client.push({
15
- number: n,
16
- now: Time.now.utc,
17
- })
18
- end
19
- }
20
- Process.waitpid pid, 0
@@ -0,0 +1,51 @@
1
+ # Usage: bundle exec ruby examples/echo_server.rb
2
+ #
3
+ # By default this starts in thread that other example scripts can use.
4
+ #
5
+ # By setting FOREGROUND=1, this will run in the foreground instead of
6
+ # background thread.
7
+ #
8
+ # FOREGROUND=1 bundle exec ruby examples/echo_server.rb
9
+ require "socket"
10
+ require "thread"
11
+ require "logger"
12
+ require "json"
13
+ require "rack"
14
+ require "rack/handler/webrick"
15
+
16
+ class EchoServer
17
+ include Singleton
18
+
19
+ attr_reader :port, :thread
20
+
21
+ def initialize
22
+ @logger = Logger.new(STDOUT)
23
+ @logger.level = Logger::INFO
24
+ @port = ENV.fetch("PORT", 9999)
25
+ @started = false
26
+
27
+ @server = WEBrick::HTTPServer.new({
28
+ Port: @port,
29
+ StartCallback: -> { @started = true },
30
+ Logger: WEBrick::Log.new(@logger, WEBrick::Log::INFO),
31
+ AccessLog: [
32
+ [@logger, WEBrick::AccessLog::COMMON_LOG_FORMAT],
33
+ ],
34
+ })
35
+
36
+ @server.mount '/', Rack::Handler::WEBrick, ->(env) {
37
+ request = Rack::Request.new(env)
38
+ @logger.debug JSON.parse(request.body.read).inspect
39
+ [200, {}, [""]]
40
+ }
41
+
42
+ @thread = Thread.new { @server.start }
43
+ Timeout.timeout(10) { :wait until @started }
44
+ end
45
+ end
46
+
47
+ EchoServer.instance
48
+
49
+ if ENV.fetch("FOREGROUND", "0") == "1"
50
+ EchoServer.instance.thread.join
51
+ end
@@ -0,0 +1,20 @@
1
+ require_relative "../lib/brow"
2
+ require_relative "echo_server"
3
+
4
+ client = Brow::Client.new({
5
+ url: "http://localhost:#{EchoServer.instance.port}",
6
+ batch_size: 10,
7
+ })
8
+
9
+ client.push({
10
+ now: Time.now.utc,
11
+ parent: true,
12
+ })
13
+
14
+ pid = fork {
15
+ client.push({
16
+ now: Time.now.utc,
17
+ child: true,
18
+ })
19
+ }
20
+ Process.waitpid pid, 0
@@ -0,0 +1,32 @@
1
+ require_relative "../lib/brow"
2
+
3
+ port = ENV.fetch("PORT") { 9999 }
4
+
5
+ if ENV.fetch("START_SERVER", "1") == "1"
6
+ require_relative "echo_server"
7
+ port = EchoServer.instance.port
8
+ end
9
+
10
+ Brow.logger = Logger.new(STDOUT)
11
+ Brow.logger.level = Logger::INFO
12
+
13
+ client = Brow::Client.new({
14
+ url: "http://localhost:#{port}",
15
+ batch_size: 1_000,
16
+ })
17
+
18
+ running = true
19
+
20
+ trap("INT") {
21
+ puts "Shutting down"
22
+ running = false
23
+ }
24
+
25
+ while running
26
+ rand(10_000).times { client.push("foo" => "bar") }
27
+
28
+ puts "Queue size: #{client.worker.queue.size}"
29
+
30
+ # Pretend to work
31
+ sleep(rand)
32
+ end
@@ -32,10 +32,30 @@ module Brow
32
32
  # :randomization_factor - The randomization factor to use to create a range
33
33
  # around the retry interval.
34
34
  def initialize(options = {})
35
- @min_timeout_ms = options[:min_timeout_ms] || MIN_TIMEOUT_MS
36
- @max_timeout_ms = options[:max_timeout_ms] || MAX_TIMEOUT_MS
37
- @multiplier = options[:multiplier] || MULTIPLIER
38
- @randomization_factor = options[:randomization_factor] || RANDOMIZATION_FACTOR
35
+ @min_timeout_ms = options.fetch(:min_timeout_ms) {
36
+ ENV.fetch("BROW_BACKOFF_MIN_TIMEOUT_MS", MIN_TIMEOUT_MS).to_i
37
+ }
38
+ @max_timeout_ms = options.fetch(:max_timeout_ms) {
39
+ ENV.fetch("BROW_BACKOFF_MAX_TIMEOUT_MS", MAX_TIMEOUT_MS).to_i
40
+ }
41
+ @multiplier = options.fetch(:multiplier) {
42
+ ENV.fetch("BROW_BACKOFF_MULTIPLIER", MULTIPLIER).to_f
43
+ }
44
+ @randomization_factor = options.fetch(:randomization_factor) {
45
+ ENV.fetch("BROW_BACKOFF_RANDOMIZATION_FACTOR", RANDOMIZATION_FACTOR).to_f
46
+ }
47
+
48
+ unless @min_timeout_ms >= 0
49
+ raise ArgumentError, ":min_timeout_ms must be >= 0 but was #{@min_timeout_ms.inspect}"
50
+ end
51
+
52
+ unless @max_timeout_ms >= 0
53
+ raise ArgumentError, ":max_timeout_ms must be >= 0 but was #{@max_timeout_ms.inspect}"
54
+ end
55
+
56
+ unless @min_timeout_ms <= max_timeout_ms
57
+ raise ArgumentError, ":min_timeout_ms (#{@min_timeout_ms.inspect}) must be <= :max_timeout_ms (#{@max_timeout_ms.inspect})"
58
+ end
39
59
 
40
60
  @attempts = 0
41
61
  end
@@ -25,11 +25,18 @@ module Brow
25
25
  def_delegators :@messages, :size
26
26
  def_delegators :@messages, :count
27
27
 
28
- attr_reader :uuid, :json_size
28
+ attr_reader :uuid, :json_size, :max_size
29
29
 
30
30
  def initialize(options = {})
31
31
  clear
32
- @max_size = options[:max_size] || MAX_SIZE
32
+ @max_size = options.fetch(:max_size) {
33
+ ENV.fetch("BROW_BATCH_SIZE", MAX_SIZE).to_i
34
+ }
35
+
36
+ unless @max_size > 0
37
+ raise ArgumentError, ":max_size must be > 0 but was #{@max_size.inspect}"
38
+ end
39
+
33
40
  @logger = options.fetch(:logger) { Brow.logger }
34
41
  end
35
42
 
@@ -3,6 +3,7 @@
3
3
  require 'net/http'
4
4
  require 'net/https'
5
5
  require 'json'
6
+ require 'set'
6
7
 
7
8
  require_relative 'response'
8
9
  require_relative 'backoff_policy'
@@ -18,20 +19,40 @@ module Brow
18
19
  # Private: Default open timeout on requests.
19
20
  OPEN_TIMEOUT = 4
20
21
 
22
+ # Private: Default write timeout on requests.
23
+ WRITE_TIMEOUT = 4
24
+
25
+ # Private: URL schemes that this transport supports.
26
+ VALID_HTTP_SCHEMES = Set["http", "https"].freeze
27
+
21
28
  # Private
22
29
  attr_reader :url, :headers, :retries, :logger, :backoff_policy, :http
23
30
 
24
31
  def initialize(options = {})
25
- @url = options[:url] || raise(ArgumentError, ":url is required to be present so we know where to send batches")
32
+ @url = options.fetch(:url) {
33
+ ENV.fetch("BROW_URL") {
34
+ raise ArgumentError, ":url is required to be present so we know where to send batches"
35
+ }
36
+ }
26
37
  @uri = URI.parse(@url)
27
38
 
39
+ unless VALID_HTTP_SCHEMES.include?(@uri.scheme)
40
+ raise ArgumentError, ":url was must be http(s) scheme but was #{@uri.scheme.inspect}"
41
+ end
42
+
28
43
  # Default path if people forget a slash.
29
44
  if @uri.path.nil? || @uri.path.empty?
30
45
  @uri.path = "/"
31
46
  end
32
47
 
33
48
  @headers = options[:headers] || {}
34
- @retries = options[:retries] || RETRIES
49
+ @retries = options.fetch(:retries) {
50
+ ENV.fetch("BROW_RETRIES", RETRIES).to_i
51
+ }
52
+
53
+ unless @retries >= 0
54
+ raise ArgumentError, ":retries must be >= to 0 but was #{@retries.inspect}"
55
+ end
35
56
 
36
57
  @logger = options.fetch(:logger) { Brow.logger }
37
58
  @backoff_policy = options.fetch(:backoff_policy) {
@@ -40,8 +61,25 @@ module Brow
40
61
 
41
62
  @http = Net::HTTP.new(@uri.host, @uri.port)
42
63
  @http.use_ssl = @uri.scheme == "https"
43
- @http.read_timeout = options[:read_timeout] || READ_TIMEOUT
44
- @http.open_timeout = options[:open_timeout] || OPEN_TIMEOUT
64
+
65
+ read_timeout = options.fetch(:read_timeout) {
66
+ ENV.fetch("BROW_READ_TIMEOUT", READ_TIMEOUT).to_f
67
+ }
68
+ @http.read_timeout = read_timeout if read_timeout
69
+
70
+ open_timeout = options.fetch(:open_timeout) {
71
+ ENV.fetch("BROW_OPEN_TIMEOUT", OPEN_TIMEOUT).to_f
72
+ }
73
+ @http.open_timeout = open_timeout if open_timeout
74
+
75
+ if RUBY_VERSION >= '2.6.0'
76
+ write_timeout = options.fetch(:write_timeout) {
77
+ ENV.fetch("BROW_WRITE_TIMEOUT", WRITE_TIMEOUT).to_f
78
+ }
79
+ @http.write_timeout = write_timeout if write_timeout
80
+ else
81
+ Kernel.warn("Warning: option :write_timeout requires Ruby version 2.6.0 or later")
82
+ end
45
83
  end
46
84
 
47
85
  # Sends a batch of messages to the API
@@ -128,7 +166,7 @@ module Brow
128
166
  headers = {
129
167
  "Accept" => "application/json",
130
168
  "Content-Type" => "application/json",
131
- "User-Agent" => "brow-ruby/#{Brow::VERSION}",
169
+ "User-Agent" => "Brow v#{Brow::VERSION}",
132
170
  "Client-Language" => "ruby",
133
171
  "Client-Language-Version" => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
134
172
  "Client-Platform" => RUBY_PLATFORM,
data/lib/brow/utils.rb CHANGED
@@ -13,13 +13,6 @@ module Brow
13
13
  end
14
14
  end
15
15
 
16
- # Internal: Return a new hash with keys converted to strings
17
- def stringify_keys(hash)
18
- hash.each_with_object({}) do |(k, v), memo|
19
- memo[k.to_s] = v
20
- end
21
- end
22
-
23
16
  # Internal: Returns a new hash with all the date values in the into
24
17
  # iso8601 strings
25
18
  def isoify_dates(hash)
data/lib/brow/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Brow
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/brow/worker.rb CHANGED
@@ -32,8 +32,8 @@ module Brow
32
32
  # The worker continuously takes messages off the queue and makes requests to
33
33
  # the api.
34
34
  #
35
- # queue - Queue synchronized between client and worker
36
35
  # options - The Hash of worker options.
36
+ # :queue - Queue synchronized between client and worker
37
37
  # :on_error - Proc of what to do on an error.
38
38
  # :batch_size - Fixnum of how many items to send in a batch.
39
39
  # :transport - The Transport object to deliver batches.
@@ -42,6 +42,8 @@ module Brow
42
42
  # via Transport.
43
43
  # :shutdown_timeout - The number of seconds to wait for the worker thread
44
44
  # to join when shutting down.
45
+ # :start_automatically - Should the client start the worker thread
46
+ # automatically and keep it running.
45
47
  # :shutdown_automatically - Should the client shutdown automatically or
46
48
  # manually. If true, shutdown is automatic. If
47
49
  # false, you'll need to handle this on your own.
@@ -52,11 +54,32 @@ module Brow
52
54
  @mutex = Mutex.new
53
55
  options = Brow::Utils.symbolize_keys(options)
54
56
  @on_error = options[:on_error] || DEFAULT_ON_ERROR
55
- @batch_size = options[:batch_size]
56
- @max_queue_size = options.fetch(:max_queue_size) { MAX_QUEUE_SIZE }
57
57
  @logger = options.fetch(:logger) { Brow.logger }
58
58
  @transport = options.fetch(:transport) { Transport.new(options) }
59
- @shutdown_timeout = options.fetch(:shutdown_timeout) { SHUTDOWN_TIMEOUT }
59
+
60
+ @batch_size = options.fetch(:batch_size) {
61
+ ENV.fetch("BROW_BATCH_SIZE", MessageBatch::MAX_SIZE).to_i
62
+ }
63
+ @max_queue_size = options.fetch(:max_queue_size) {
64
+ ENV.fetch("BROW_MAX_QUEUE_SIZE", MAX_QUEUE_SIZE).to_i
65
+ }
66
+ @shutdown_timeout = options.fetch(:shutdown_timeout) {
67
+ ENV.fetch("BROW_SHUTDOWN_TIMEOUT", SHUTDOWN_TIMEOUT).to_f
68
+ }
69
+
70
+ if @batch_size <= 0
71
+ raise ArgumentError, ":batch_size must be greater than 0"
72
+ end
73
+
74
+ if @max_queue_size <= 0
75
+ raise ArgumentError, ":max_queue_size must be greater than 0"
76
+ end
77
+
78
+ if @shutdown_timeout <= 0
79
+ raise ArgumentError, ":shutdown_timeout must be greater than 0"
80
+ end
81
+
82
+ @start_automatically = options.fetch(:start_automatically, true)
60
83
 
61
84
  if options.fetch(:shutdown_automatically, true)
62
85
  at_exit { stop }
@@ -65,9 +88,9 @@ module Brow
65
88
 
66
89
  def push(data)
67
90
  raise ArgumentError, "data must be a Hash" unless data.is_a?(Hash)
68
- start
91
+ start if @start_automatically
69
92
 
70
- data = Utils.isoify_dates(Utils.stringify_keys(data))
93
+ data = Utils.isoify_dates(data)
71
94
 
72
95
  if queue.length < max_queue_size
73
96
  queue << data
@@ -108,6 +131,7 @@ module Brow
108
131
 
109
132
  case message
110
133
  when SHUTDOWN
134
+ logger.info("[brow]") { "Worker shutting down" }
111
135
  send_batch(batch) unless batch.empty?
112
136
  break
113
137
  else
@@ -131,12 +155,15 @@ module Brow
131
155
  end
132
156
 
133
157
  def ensure_worker_running
158
+ # Return early if thread is alive and avoid the mutex lock and unlock.
159
+ return if thread_alive?
160
+
134
161
  # If another thread is starting worker thread, then return early so this
135
162
  # thread can enqueue and move on with life.
136
163
  return unless mutex.try_lock
137
164
 
138
165
  begin
139
- return if @thread && @thread.alive?
166
+ return if thread_alive?
140
167
  @thread = Thread.new { run }
141
168
  logger.debug("[brow]") { "Worker thread [#{@thread.object_id}] started" }
142
169
  ensure
@@ -144,6 +171,10 @@ module Brow
144
171
  end
145
172
  end
146
173
 
174
+ def thread_alive?
175
+ @thread && @thread.alive?
176
+ end
177
+
147
178
  def reset
148
179
  @pid = Process.pid
149
180
  mutex.unlock if mutex.locked?
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.3.0
4
+ version: 0.4.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-29 00:00:00.000000000 Z
11
+ date: 2021-11-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -26,6 +26,9 @@ files:
26
26
  - bin/console
27
27
  - bin/setup
28
28
  - examples/basic.rb
29
+ - examples/echo_server.rb
30
+ - examples/forked.rb
31
+ - examples/long_running.rb
29
32
  - lib/brow.rb
30
33
  - lib/brow/backoff_policy.rb
31
34
  - lib/brow/client.rb