beaneater 1.3.0 → 1.3.1
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 +5 -0
- data/Rakefile +51 -1
- data/lib/beaneater/connection.rb +2 -2
- data/lib/beaneater/job/collection.rb +2 -0
- data/lib/beaneater/stats/stat_struct.rb +11 -2
- data/lib/beaneater/version.rb +1 -1
- data/test/jobs_test.rb +48 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69fc39a3d9d9dc68b793a7b82db8ccac3226c1729c163a966016f49ddd85607a
|
|
4
|
+
data.tar.gz: 7112d6b2669da5a2971bee248e9321cb3717d1023289b5ec7bba0bdcdf37a12e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5669fb49b06d4714edbb3285991a8f184d7505681abc395f9bad064a9ed4984b1be06d64e76b002f88858916321ab6bc9885114cea9fe54e79151ab4a8f6b98c
|
|
7
|
+
data.tar.gz: 0ac491e1e6b02dbbc0a15c7b89439cbaa15f7cf191f39896336b87b02505a8414713063fdb752c885eed1dea4a6a823a5b160f11e509f2d999f132426ccf4cb7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# CHANGELOG for Beaneater
|
|
2
2
|
|
|
3
|
+
## 1.3.1 (November 4 2026)
|
|
4
|
+
|
|
5
|
+
* `jobs.process!` no longer checks with the server on a job it has just deleted, or released for another try, before moving on (the check sent `stats-job` and could never bury the job): a job whose handler finishes now takes 3 commands (`reserve`, `stats-job`, `delete`) instead of 4, and a retried job takes 2 fewer. A job whose handler raised and that isn't released is still buried as before (@nesquena)
|
|
6
|
+
* `client.stats`, `tube.stats` and `job.stats` parse the server's reply with less work, so polling stats takes noticeably less CPU; the results are unchanged (@nesquena)
|
|
7
|
+
|
|
3
8
|
## 1.3.0 (November 1 2026)
|
|
4
9
|
|
|
5
10
|
* Check tube names, job ids and numbers before writing them into a command. A tube name beanstalkd does not accept (up to 200 letters, digits and `-+/;.$_()`, not starting with a hyphen) raises `InvalidTubeName` from every tube command (`use`, `watch`, `ignore`, `put`, `peek`, `kick`, `stats`, `pause`, and restoring the watched tubes on reconnect); names such as `demo.test.foo`, `jobs(eu).mailers` and `MyApp.mailers` stay valid. `watch`'s `InvalidTubeName` message now reads "Tube cannot be named '<name>'", as `use`'s does, and `tubes.watch(:t)` and `tubes.ignore(:t)` return the names as Strings. A job id that is not a whole number (an Integer or a String of digits), a priority, delay, TTR or pause delay that is not a whole number from 0 to 4294967295 (the range beanstalkd reads), a reserve timeout or kick count that is neither a whole number nor a Float, and a peek state other than `ready`, `delayed` or `buried` raise `ArgumentError`. Such values used to be sent as given and fail on the server with `BadFormatError` or `UnknownCommandError`, or be misread. What to do: `tube.stats`, `tube.pause` and `tubes.ignore` with such a tube name now raise `InvalidTubeName` instead of `BadFormatError`, and the values above raise `ArgumentError` (`jobs.find("abc")`, `tube.put(body, :ttr => 1.5)`, `tube.peek(:reserved)`) instead of `BadFormatError` or `UnknownCommandError`, both `Beaneater::UnexpectedResponse`s; code that rescues `Beaneater::UnexpectedResponse` around these calls should also rescue `Beaneater::InvalidTubeName` and `ArgumentError` (@nesquena)
|
data/Rakefile
CHANGED
|
@@ -71,7 +71,8 @@ task "release:check", [:remote] do |_, args|
|
|
|
71
71
|
if local_tag.nil? || remote_tag.nil?
|
|
72
72
|
errors << "couldn't check for tag #{tag}"
|
|
73
73
|
elsif !(local_tag.empty? && remote_tag.empty?)
|
|
74
|
-
|
|
74
|
+
page = args[:remote] ? "rake 'release:github[#{args[:remote]}]'" : "rake release:github"
|
|
75
|
+
errors << "tag #{tag} already exists (if only the gem push failed: gem push pkg/beaneater-#{version}.gem, then #{page})"
|
|
75
76
|
end
|
|
76
77
|
|
|
77
78
|
begin
|
|
@@ -87,4 +88,53 @@ end
|
|
|
87
88
|
Rake::Task["release"].prerequisites.unshift("release:check")
|
|
88
89
|
%w[release:source_control_push release:rubygem_push].each { |t| Rake::Task[t].enhance(["release:check"]) }
|
|
89
90
|
|
|
91
|
+
# rake release:github (rake release runs it last, once the tag and the gem are pushed):
|
|
92
|
+
# creates the GitHub release page for the tag through the gh CLI, on the github.com
|
|
93
|
+
# repository the remote pushes to, with a link to the CHANGELOG and GitHub's generated
|
|
94
|
+
# notes since the previous version's tag. It is marked Latest only when no higher version
|
|
95
|
+
# is tagged, so a release from an X.Y-stable branch doesn't take that from a newer one.
|
|
96
|
+
# If the page already exists it does nothing, so it can also be run by hand to finish a release.
|
|
97
|
+
desc "Create the GitHub release page for v#{Beaneater::VERSION}"
|
|
98
|
+
task "release:github", [:remote] do |_, args|
|
|
99
|
+
version = Gem::Version.new(Beaneater::VERSION)
|
|
100
|
+
tag = "v#{version}"
|
|
101
|
+
git = ->(*a) { out = IO.popen(["git", *a], err: File::NULL, &:read); out.strip if $?.success? }
|
|
102
|
+
branch = git.("rev-parse", "--abbrev-ref", "HEAD").to_s
|
|
103
|
+
remote = args[:remote] || git.("config", "--get", "branch.#{branch}.remote") || "origin"
|
|
104
|
+
url = git.("remote", "get-url", "--push", remote).to_s.split("\n").first.to_s
|
|
105
|
+
repo = url[%r{(?:\A|[@/.])github\.com[:/]([^/]+/[^/]+?)(?:\.git)?/?\z}i, 1] or abort "#{remote} isn't a GitHub repository"
|
|
106
|
+
# the push URL's tags, not the fetch URL's: the page goes to the repository the tag was pushed to
|
|
107
|
+
tags = git.("ls-remote", "--tags", url, "refs/tags/v*") or abort "Couldn't list the tags on #{remote}"
|
|
108
|
+
tagged = tags.split("\n").filter_map { |l| l[%r{\trefs/tags/v(\d+\.\d+\.\d+)\z}, 1] }.map { |v| Gem::Version.new(v) }
|
|
109
|
+
previous = tagged.select { |v| v < version }.max
|
|
110
|
+
options = ["--latest=#{tagged.none? { |v| v > version }}"]
|
|
111
|
+
options += ["--notes-start-tag", "v#{previous}"] if previous
|
|
112
|
+
# gh adds the generated notes after a single newline; the second one keeps them out of this paragraph
|
|
113
|
+
notes = "See [CHANGELOG.md](https://github.com/#{repo}/blob/#{tag}/CHANGELOG.md) for what changed, and what to do when upgrading.\n"
|
|
114
|
+
gh_repo = "github.com/#{repo}" # so an exported GH_HOST doesn't send it elsewhere
|
|
115
|
+
|
|
116
|
+
if system("gh", "release", "view", tag, "-R", gh_repo, out: File::NULL, err: File::NULL)
|
|
117
|
+
puts "GitHub release #{tag} already exists on #{repo}"
|
|
118
|
+
elsif system("gh", "release", "create", tag, "-R", gh_repo, "--verify-tag", "--title", tag, *options,
|
|
119
|
+
"--generate-notes", "--notes", notes)
|
|
120
|
+
puts "Created GitHub release #{tag} on #{repo}"
|
|
121
|
+
else
|
|
122
|
+
abort "Couldn't create GitHub release #{tag} on #{repo} (is gh installed and signed in?)"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
# The gem is out by then, so a failure here only warns and says how to finish.
|
|
126
|
+
Rake::Task["release"].enhance do |_, args|
|
|
127
|
+
if %w[n no nil false off 0].include?(ENV["gem_push"].to_s.downcase)
|
|
128
|
+
puts "gem_push is off, so no GitHub release page"
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
begin
|
|
132
|
+
Rake::Task["release:github"].invoke(args[:remote])
|
|
133
|
+
rescue SystemExit, StandardError => e
|
|
134
|
+
warn "#{e.class}: #{e.message} (#{e.backtrace&.first})" unless e.is_a?(SystemExit)
|
|
135
|
+
page = args[:remote] ? "rake 'release:github[#{args[:remote]}]'" : "rake release:github"
|
|
136
|
+
warn "The tag and the gem are pushed; only the GitHub release page is missing. Run: #{page}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
90
140
|
task :default => 'test:full'
|
data/lib/beaneater/connection.rb
CHANGED
|
@@ -414,9 +414,9 @@ class Beaneater
|
|
|
414
414
|
value[1..-2]
|
|
415
415
|
elsif text
|
|
416
416
|
value
|
|
417
|
-
elsif value
|
|
417
|
+
elsif value.match?(/\A-?\d+\z/)
|
|
418
418
|
value.to_i
|
|
419
|
-
elsif value
|
|
419
|
+
elsif value.match?(/\A-?\d+\.\d+\z/)
|
|
420
420
|
value.to_f
|
|
421
421
|
elsif value == 'true' || value == 'false'
|
|
422
422
|
value == 'true'
|
|
@@ -131,9 +131,11 @@ class Beaneater
|
|
|
131
131
|
begin
|
|
132
132
|
processor[:block].call(@current_job)
|
|
133
133
|
@current_job.delete
|
|
134
|
+
@current_job = nil # deleted, so there is nothing for the ensure below to bury
|
|
134
135
|
rescue *processor[:retry_on]
|
|
135
136
|
if @current_job.stats.releases < processor[:max_retries]
|
|
136
137
|
@current_job.release(:delay => release_delay)
|
|
138
|
+
@current_job = nil # released for another try, so no longer ours to bury
|
|
137
139
|
end
|
|
138
140
|
end
|
|
139
141
|
rescue AbortProcessingError
|
|
@@ -13,10 +13,19 @@ class Beaneater
|
|
|
13
13
|
#
|
|
14
14
|
def self.from_hash(hash)
|
|
15
15
|
return unless hash.is_a?(Hash)
|
|
16
|
-
underscore_hash = hash.inject({}) { |r, (k, v)| r[k
|
|
16
|
+
underscore_hash = hash.inject({}) { |r, (k, v)| r[underscore(k)] = v; r }
|
|
17
17
|
self.new(underscore_hash)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
# The key as a String with its hyphens turned into underscores, with String#tr for speed. A key
|
|
21
|
+
# in an encoding that isn't ASCII-compatible (UTF-16, say) takes gsub, so that the
|
|
22
|
+
# Encoding::CompatibilityError it raises keeps gsub's message.
|
|
23
|
+
def self.underscore(key)
|
|
24
|
+
key = key.to_s
|
|
25
|
+
key.encoding.ascii_compatible? ? key.tr('-', '_') : key.gsub(/-/, '_')
|
|
26
|
+
end
|
|
27
|
+
private_class_method :underscore
|
|
28
|
+
|
|
20
29
|
# Access value for stat with specified key.
|
|
21
30
|
#
|
|
22
31
|
# Keys are stored underscored (see {.from_hash}), so hyphenated beanstalkd
|
|
@@ -30,7 +39,7 @@ class Beaneater
|
|
|
30
39
|
# @stats['current-jobs-ready'] # => 5
|
|
31
40
|
#
|
|
32
41
|
def [](key)
|
|
33
|
-
name =
|
|
42
|
+
name = self.class.send(:underscore, key)
|
|
34
43
|
@hash.fetch(name) { @hash[name.to_sym] }
|
|
35
44
|
end
|
|
36
45
|
|
data/lib/beaneater/version.rb
CHANGED
data/test/jobs_test.rb
CHANGED
|
@@ -118,6 +118,54 @@ describe Beaneater::Jobs do
|
|
|
118
118
|
end
|
|
119
119
|
end # for_process!
|
|
120
120
|
|
|
121
|
+
describe "for process! when it deletes or releases a job" do
|
|
122
|
+
before do
|
|
123
|
+
$ran = []
|
|
124
|
+
cleanup_tubes!(['tube_done', 'tube_retry'])
|
|
125
|
+
@sent = []
|
|
126
|
+
sent = @sent
|
|
127
|
+
@beanstalk.connection.define_singleton_method(:transmit) do |command, **options|
|
|
128
|
+
sent << command.to_s
|
|
129
|
+
super(command, **options)
|
|
130
|
+
end
|
|
131
|
+
@jobs.register('tube_retry', :retry_on => [Timeout::Error], :max_retries => 1) do |job|
|
|
132
|
+
$ran << job.id
|
|
133
|
+
raise Timeout::Error
|
|
134
|
+
end
|
|
135
|
+
@jobs.register('tube_done') { |job| $ran << job.id; @jobs.stop! if $ran.size == 4 }
|
|
136
|
+
@retry_id = @beanstalk.tubes.find('tube_retry').put("retry", :pri => 1)[:id]
|
|
137
|
+
@done_ids = 2.times.map { |i| @beanstalk.tubes.find('tube_done').put("done #{i}", :pri => 2)[:id] }
|
|
138
|
+
Timeout.timeout(10) { @jobs.process!(:release_delay => 0, :reserve_timeout => 1) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
after do
|
|
142
|
+
connection = @beanstalk.connection
|
|
143
|
+
connection.singleton_class.send(:remove_method, :transmit) if connection.singleton_methods(false).include?(:transmit)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The commands naming job id after the given command, up to the next reserve
|
|
147
|
+
def sent_about(id, command)
|
|
148
|
+
start = @sent.index(command)
|
|
149
|
+
refute_nil start, "#{command.inspect} was not sent: #{@sent.inspect}"
|
|
150
|
+
@sent.drop(start + 1).take_while { |c| !c.start_with?('reserve') }.select { |c| c.split(' ')[1] == id.to_s }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "sends nothing more about a job it deleted" do
|
|
154
|
+
@done_ids.each do |id|
|
|
155
|
+
assert_empty sent_about(id, "delete #{id}"), "sent after deleting job #{id}: #{@sent.inspect}"
|
|
156
|
+
end
|
|
157
|
+
stats = @beanstalk.tubes.find('tube_done').stats
|
|
158
|
+
assert_equal [0, 0, 0], [stats.current_jobs_ready, stats.current_jobs_reserved, stats.current_jobs_buried]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "sends nothing more about a job it released, and buries it when it runs out of retries" do
|
|
162
|
+
assert_empty sent_about(@retry_id, "release #{@retry_id} 1 0"), "sent after releasing job #{@retry_id}: #{@sent.inspect}"
|
|
163
|
+
assert_equal [@retry_id, @retry_id], $ran.first(2)
|
|
164
|
+
job = @beanstalk.jobs.find(@retry_id)
|
|
165
|
+
assert_equal ['buried', 1], [job.stats.state, job.stats.releases]
|
|
166
|
+
end
|
|
167
|
+
end # process! when it deletes or releases a job
|
|
168
|
+
|
|
121
169
|
describe "for process! with a dashed tube name" do
|
|
122
170
|
before do
|
|
123
171
|
$ran = []
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: beaneater
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nico Taing
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|