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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f50f38f9e485e31b15278b6fdff9a8d74e13aa6fc3e0c9f0c366b5091f5b2d5
4
- data.tar.gz: b16e2755db5a25f216a1b4577b925536edebfd304c5e497ea8189dbb8630cf6e
3
+ metadata.gz: 69fc39a3d9d9dc68b793a7b82db8ccac3226c1729c163a966016f49ddd85607a
4
+ data.tar.gz: 7112d6b2669da5a2971bee248e9321cb3717d1023289b5ec7bba0bdcdf37a12e
5
5
  SHA512:
6
- metadata.gz: 07c0c6c6032c57630e23515ab53549379de75ba2e985449ac849003e57f5317663dda129aa1bb89f8b61e1fa535c7f385373a009e79043cd6cfb9cd00e3c7ac9
7
- data.tar.gz: 04c2851609baa2d9b73057ad45567093bba58e1689ee418f967c547388be683193fa01a73a18a4b2e8401c632a6f0a49cceeaa56c352db8540e89aba764fd438
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
- errors << "tag #{tag} already exists (if only the gem push failed: gem push pkg/beaneater-#{version}.gem)"
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'
@@ -414,9 +414,9 @@ class Beaneater
414
414
  value[1..-2]
415
415
  elsif text
416
416
  value
417
- elsif value =~ /\A-?\d+\z/
417
+ elsif value.match?(/\A-?\d+\z/)
418
418
  value.to_i
419
- elsif value =~ /\A-?\d+\.\d+\z/
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.to_s.gsub(/-/, '_')] = v; r }
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 = key.to_s.gsub(/-/, '_')
42
+ name = self.class.send(:underscore, key)
34
43
  @hash.fetch(name) { @hash[name.to_sym] }
35
44
  end
36
45
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  class Beaneater
4
4
  # Current version of gem.
5
- VERSION = "1.3.0"
5
+ VERSION = "1.3.1"
6
6
  end
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.0
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-01 00:00:00.000000000 Z
11
+ date: 2026-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest