backburner 1.7.0 → 1.8.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/.github/workflows/ci.yml +45 -0
- data/.github/workflows/github-release.yml +58 -0
- data/.github/workflows/release.yml +221 -0
- data/CHANGELOG.md +29 -0
- data/Gemfile +4 -1
- data/HOOKS.md +23 -14
- data/README.md +32 -9
- data/backburner.gemspec +3 -0
- data/bin/backburner +1 -1
- data/lib/active_job/queue_adapters/backburner_adapter.rb +10 -3
- data/lib/backburner/configuration.rb +7 -0
- data/lib/backburner/connection.rb +4 -2
- data/lib/backburner/enqueue_pool.rb +88 -0
- data/lib/backburner/helpers.rb +77 -22
- data/lib/backburner/job.rb +46 -7
- data/lib/backburner/logger.rb +9 -0
- data/lib/backburner/performable.rb +1 -1
- data/lib/backburner/queue.rb +37 -19
- data/lib/backburner/version.rb +1 -1
- data/lib/backburner/worker.rb +112 -21
- data/lib/backburner/workers/forking.rb +4 -2
- data/lib/backburner/workers/simple.rb +5 -0
- data/lib/backburner/workers/threading.rb +1 -7
- data/lib/backburner/workers/threads_on_fork.rb +58 -23
- data/lib/backburner.rb +7 -1
- data/test/active_job_adapter_test.rb +24 -0
- data/test/back_burner_test.rb +13 -0
- data/test/connection_test.rb +22 -0
- data/test/fixtures/active_job_app.rb +53 -0
- data/test/fixtures/autoload/test_broken_name_job.rb +4 -0
- data/test/fixtures/autoload/test_broken_no_method_job.rb +4 -0
- data/test/fixtures/autoload/test_broken_require_job.rb +4 -0
- data/test/fixtures/autoload/test_broken_runtime_job.rb +4 -0
- data/test/fixtures/autoload/test_broken_syntax_job.rb +4 -0
- data/test/fixtures/autoload/test_broken_timeout_job.rb +4 -0
- data/test/fixtures/autoload/test_same_name_job.rb +5 -0
- data/test/fixtures/test_fork_jobs.rb +5 -0
- data/test/fixtures/test_jobs.rb +53 -0
- data/test/fixtures/test_queue_settings.rb +41 -1
- data/test/helpers/templogger.rb +1 -1
- data/test/helpers_test.rb +184 -0
- data/test/job_test.rb +22 -0
- data/test/logger_test.rb +27 -0
- data/test/performable_test.rb +50 -0
- data/test/queue_test.rb +63 -0
- data/test/test_helper.rb +57 -0
- data/test/worker_test.rb +346 -1
- data/test/workers/forking_worker_test.rb +19 -1
- data/test/workers/simple_worker_test.rb +272 -0
- data/test/workers/threading_worker_test.rb +22 -1
- data/test/workers/threads_on_fork_worker_test.rb +279 -12
- metadata +54 -6
- data/.travis.yml +0 -29
- data/circle.yml +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50e2f0b2644659644c0789a09a93c786e1bf7ab914bdadc40e80be8cdf941fb7
|
|
4
|
+
data.tar.gz: e86d98085d8fbeb91079d9d52095d2ba62f9c4ec114e28053c8d457398003b35
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f707e632bc937197959a4e919b8f12b010a201b7e18add316c3cb66bf1598219a1089c480f7012f9d888debc63fc1d0c4eb0be85b4a943ac3220dce2453c22e6
|
|
7
|
+
data.tar.gz: c79112e436fd4bff2b68cc417de9fe23b782b433882697bae60b937f8c36ae76177e056a3a86cff1ab714da48a59d5a548e8113469382a387967295dc697aee1
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "master" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "master" ]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: test (${{ matrix.ruby-version }}${{ matrix.activejob-version && format(', activejob {0}', matrix.activejob-version) || '' }})
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
|
|
20
|
+
activejob-version: [''] # the newest the Ruby version allows
|
|
21
|
+
include:
|
|
22
|
+
# Rails before 7.2 ship their own Active Job adapter and have no AbstractAdapter
|
|
23
|
+
- ruby-version: '3.1'
|
|
24
|
+
activejob-version: '6.1'
|
|
25
|
+
env:
|
|
26
|
+
ACTIVEJOB_VERSION: ${{ matrix.activejob-version }} # read by the Gemfile
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- name: Download & Extract beanstalkd
|
|
31
|
+
run: curl -fsSL https://github.com/beanstalkd/beanstalkd/archive/refs/tags/v1.13.tar.gz | tar xz
|
|
32
|
+
- name: Make beanstalkd
|
|
33
|
+
run: make
|
|
34
|
+
working-directory: beanstalkd-1.13
|
|
35
|
+
- name: Set up Ruby
|
|
36
|
+
uses: ruby/setup-ruby@v1
|
|
37
|
+
with:
|
|
38
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
39
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
40
|
+
- name: Daemonize beanstalkd
|
|
41
|
+
run: ./beanstalkd-1.13/beanstalkd -l 127.0.0.1 -p 11300 &
|
|
42
|
+
- name: Run tests
|
|
43
|
+
run: bundle exec rake test
|
|
44
|
+
env:
|
|
45
|
+
BEANSTALKD_BIN: ${{ github.workspace }}/beanstalkd-1.13/beanstalkd # for the outage test, which stops and restarts its own server
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Creates the GitHub release page for a tag that already exists, with GitHub's generated notes (merged
|
|
2
|
+
# PRs and new contributors since the previous release) after a link to that version's CHANGELOG.md.
|
|
3
|
+
# release.yml runs it after it publishes; "Run workflow" here (with the tag) creates the page for an
|
|
4
|
+
# earlier release. A tag that already has a release is left alone, so running it twice is harmless.
|
|
5
|
+
# A tag that isn't the newest vX.Y.Z tag is created with --latest=false; the API otherwise marks every
|
|
6
|
+
# new release as Latest.
|
|
7
|
+
|
|
8
|
+
name: github-release
|
|
9
|
+
run-name: github release ${{ inputs.tag }}
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
inputs:
|
|
14
|
+
tag:
|
|
15
|
+
description: 'An existing tag, e.g. v1.2.0'
|
|
16
|
+
required: true
|
|
17
|
+
type: string
|
|
18
|
+
workflow_call:
|
|
19
|
+
inputs:
|
|
20
|
+
tag:
|
|
21
|
+
required: true
|
|
22
|
+
type: string
|
|
23
|
+
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
release-page:
|
|
29
|
+
if: github.repository == 'nesquena/backburner'
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
permissions:
|
|
32
|
+
contents: write # creates the release
|
|
33
|
+
steps:
|
|
34
|
+
- name: Create the GitHub release
|
|
35
|
+
shell: bash -eo pipefail {0}
|
|
36
|
+
env:
|
|
37
|
+
TAG: ${{ inputs.tag }}
|
|
38
|
+
CHANGELOG: CHANGELOG.md
|
|
39
|
+
GH_TOKEN: ${{ github.token }}
|
|
40
|
+
run: |
|
|
41
|
+
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::'$TAG' is not a tag like v1.2.3"; exit 1; }
|
|
42
|
+
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
|
43
|
+
echo "$TAG already has a release; leaving it alone"
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
# Compare with the newest vX.Y.Z tag rather than the "Latest" release, so a failed lookup stops the
|
|
47
|
+
# step (pipefail) instead of letting an older tag become Latest
|
|
48
|
+
newest=$(gh api --paginate "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" --jq '.[].ref' |
|
|
49
|
+
sed 's#^refs/tags/##' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1)
|
|
50
|
+
flags=()
|
|
51
|
+
if [ "$newest" != "$TAG" ]; then
|
|
52
|
+
echo "$TAG is not the newest tag ($newest): not marking it as latest"
|
|
53
|
+
flags=(--latest=false)
|
|
54
|
+
fi
|
|
55
|
+
# --verify-tag: refuse rather than create the tag. With --generate-notes, GitHub puts these notes
|
|
56
|
+
# before the generated ones.
|
|
57
|
+
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --title "$TAG" --generate-notes "${flags[@]}" \
|
|
58
|
+
--notes "What changed, and what to do when upgrading: [$CHANGELOG](https://github.com/$GITHUB_REPOSITORY/blob/$TAG/$CHANGELOG)."
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Publishes a backburner release to rubygems.org with trusted publishing, gated by the "release"
|
|
2
|
+
# environment: the owner approves each publish.
|
|
3
|
+
#
|
|
4
|
+
# Usual path: merge the release PR (the version in lib/backburner/version.rb and the CHANGELOG.md
|
|
5
|
+
# heading stamped with the date, e.g. "## Version 1.8.0 (Nov 1 2026)"), then "Run workflow" on master
|
|
6
|
+
# with the version. build checks the version, that the changelog's first heading is that version with
|
|
7
|
+
# a date, and the branch, and builds the gem reproducibly; publish waits for approval, pushes the gem
|
|
8
|
+
# with short-lived trusted-publishing credentials, then creates the tag vX.Y.Z on the built commit.
|
|
9
|
+
# Pushing a vX.Y.Z tag on a master commit runs the same checks. "Re-run failed jobs" (not "Re-run all
|
|
10
|
+
# jobs", which rebuilds) resumes a release that stopped partway.
|
|
11
|
+
# If the gem is pushed but the tag step keeps failing, push the tag by hand at the built commit: that
|
|
12
|
+
# tag push's run skips the push (same sum).
|
|
13
|
+
# Then github-release.yml creates the GitHub release page for the tag (it can also be run by hand for
|
|
14
|
+
# an earlier tag).
|
|
15
|
+
#
|
|
16
|
+
# Dry run: tick dry_run to check the wiring. Everything runs except the changelog checks, the tag, the
|
|
17
|
+
# push and the release page; the credential exchange fails unless the trusted publisher is set up.
|
|
18
|
+
#
|
|
19
|
+
# The "release" environment's protection rules are the ONLY gate on publishing (required reviewer, no
|
|
20
|
+
# admin bypass, deployments only from master and v*.*.* tags): RubyGems trusts any run of this file
|
|
21
|
+
# that uses that environment, and a branch or tag can carry its own modified copy of it. The trusted
|
|
22
|
+
# publisher on rubygems.org must name the environment. Before approving a deployment, check
|
|
23
|
+
# who started the run, the event, that the commit is the release PR's merge commit, and the build
|
|
24
|
+
# log's "is on master" line. Reject any run you won't approve: a waiting run blocks the next one.
|
|
25
|
+
# A deployment rule for X.Y-stable lets anyone with write access create such a branch carrying its own
|
|
26
|
+
# copy of this file: add one only for a maintenance release, and remove it after.
|
|
27
|
+
|
|
28
|
+
name: release
|
|
29
|
+
run-name: release ${{ inputs.version || github.ref_name }}${{ inputs.dry_run && ' (dry run)' || '' }}
|
|
30
|
+
|
|
31
|
+
on:
|
|
32
|
+
push:
|
|
33
|
+
tags: ['v[0-9]+.[0-9]+.[0-9]+']
|
|
34
|
+
workflow_dispatch:
|
|
35
|
+
inputs:
|
|
36
|
+
version:
|
|
37
|
+
description: 'Version to release (already in the version file of this branch), e.g. 1.2.0'
|
|
38
|
+
required: true
|
|
39
|
+
type: string
|
|
40
|
+
dry_run:
|
|
41
|
+
description: 'Dry run: check, build and exchange credentials, but create no tag and push nothing'
|
|
42
|
+
required: false
|
|
43
|
+
type: boolean
|
|
44
|
+
default: false
|
|
45
|
+
|
|
46
|
+
permissions:
|
|
47
|
+
contents: read
|
|
48
|
+
|
|
49
|
+
concurrency:
|
|
50
|
+
group: release
|
|
51
|
+
cancel-in-progress: false
|
|
52
|
+
|
|
53
|
+
jobs:
|
|
54
|
+
build:
|
|
55
|
+
if: github.repository == 'nesquena/backburner'
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
env:
|
|
58
|
+
TRUNK: master
|
|
59
|
+
TAG_PREFIX: v
|
|
60
|
+
GEMS: 'backburner:.'
|
|
61
|
+
VERSION_FILE: lib/backburner/version.rb
|
|
62
|
+
CHANGELOG: CHANGELOG.md
|
|
63
|
+
CHANGELOG_HEADING: '^## '
|
|
64
|
+
CHANGELOG_TOP: '^## Version ([0-9]+\.[0-9]+\.[0-9]+)( |$)'
|
|
65
|
+
outputs:
|
|
66
|
+
version: ${{ steps.check.outputs.version }}
|
|
67
|
+
tag: ${{ steps.check.outputs.tag }}
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
70
|
+
with:
|
|
71
|
+
fetch-depth: 0
|
|
72
|
+
submodules: true
|
|
73
|
+
persist-credentials: false
|
|
74
|
+
- uses: ruby/setup-ruby@de492cf36cbab201753d720ce4f4e336cd688c94 # v1.336.0
|
|
75
|
+
with:
|
|
76
|
+
ruby-version: '3.3'
|
|
77
|
+
- name: Check the version
|
|
78
|
+
id: check
|
|
79
|
+
shell: bash -eo pipefail {0}
|
|
80
|
+
env:
|
|
81
|
+
INPUT_VERSION: ${{ inputs.version }}
|
|
82
|
+
DRY_RUN: ${{ inputs.dry_run }}
|
|
83
|
+
run: |
|
|
84
|
+
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then
|
|
85
|
+
version="$INPUT_VERSION"
|
|
86
|
+
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::the version input is not a plain version like 1.2.3"; exit 1; }
|
|
87
|
+
case "$GITHUB_REF" in
|
|
88
|
+
"refs/heads/$TRUNK"|"refs/heads/${version%.*}-stable") ;;
|
|
89
|
+
*) echo "::error::run this on $TRUNK or ${version%.*}-stable, not $GITHUB_REF"; exit 1 ;;
|
|
90
|
+
esac
|
|
91
|
+
tag="$TAG_PREFIX$version"
|
|
92
|
+
tagged=$(git ls-remote origin "refs/tags/$tag*" | awk -v t="refs/tags/$tag" '$2 == t || $2 == t "^{}" { print $1 }')
|
|
93
|
+
[ -z "$tagged" ] || echo "$tagged" | grep -qx "$GITHUB_SHA" ||
|
|
94
|
+
{ echo "::error::tag $tag already exists at another commit"; exit 1; }
|
|
95
|
+
else
|
|
96
|
+
tag="$GITHUB_REF_NAME"
|
|
97
|
+
version="${tag#"$TAG_PREFIX"}"
|
|
98
|
+
[[ "$tag" == "$TAG_PREFIX"* && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::tag $tag is not ${TAG_PREFIX}X.Y.Z"; exit 1; }
|
|
99
|
+
fi
|
|
100
|
+
file_version=$(ruby -e 'puts File.binread(ENV["VERSION_FILE"])[/\bVERSION\s*=\s*["\x27]([^"\x27]+)/, 1]')
|
|
101
|
+
[ "$version" = "$file_version" ] || { echo "::error::version $version but $VERSION_FILE says $file_version"; exit 1; }
|
|
102
|
+
if [ "$DRY_RUN" = true ]; then
|
|
103
|
+
echo "::warning::dry run: not checking $CHANGELOG"
|
|
104
|
+
else
|
|
105
|
+
heading=$(grep -m1 -E "$CHANGELOG_HEADING" "$CHANGELOG") || { echo "::error::$CHANGELOG has no heading matching '$CHANGELOG_HEADING'"; exit 1; }
|
|
106
|
+
top=$(echo "$heading" | sed -nE "s/$CHANGELOG_TOP.*/\1/p")
|
|
107
|
+
[ "$top" = "$version" ] || { echo "::error::$CHANGELOG starts with '$heading', not release $version: merge the release PR first, and nothing after it"; exit 1; }
|
|
108
|
+
rest="${heading#*"$version"}"
|
|
109
|
+
[[ "$rest" =~ (19|20)[0-9][0-9] ]] && ! echo "$rest" | grep -qiwE 'unreleased|tbd|tba' ||
|
|
110
|
+
{ echo "::error::$CHANGELOG's '$heading' isn't stamped with a date yet"; exit 1; }
|
|
111
|
+
fi
|
|
112
|
+
series="${version%.*}-stable"
|
|
113
|
+
on=""
|
|
114
|
+
for b in "$series" "$TRUNK"; do
|
|
115
|
+
git merge-base --is-ancestor "$GITHUB_SHA" "refs/remotes/origin/$b" 2>/dev/null && on="$b"
|
|
116
|
+
done
|
|
117
|
+
[ -n "$on" ] || { echo "::error::$version ($GITHUB_SHA) is not on $TRUNK or $series"; exit 1; }
|
|
118
|
+
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
119
|
+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
|
120
|
+
echo "$version ($GITHUB_SHA) is on $on"
|
|
121
|
+
- name: Build the gems
|
|
122
|
+
shell: bash -eo pipefail {0}
|
|
123
|
+
env:
|
|
124
|
+
VERSION: ${{ steps.check.outputs.version }}
|
|
125
|
+
run: |
|
|
126
|
+
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct "$GITHUB_SHA"); export SOURCE_DATE_EPOCH
|
|
127
|
+
mkdir dist
|
|
128
|
+
for spec in $GEMS; do
|
|
129
|
+
g="${spec%%:*}"; dir="${spec#*:}"; [ "$dir" = "$spec" ] && dir=.
|
|
130
|
+
out="$GITHUB_WORKSPACE/dist/$g-$VERSION.gem"
|
|
131
|
+
(cd "$dir" && gem build "$g.gemspec" --output "$out")
|
|
132
|
+
[ "$(gem spec "$out" name)" = "--- $g" ] && [ "$(gem spec "$out" version | sed -n 's/^version: //p')" = "$VERSION" ] ||
|
|
133
|
+
{ echo "::error::$out is not $g $VERSION:"; gem spec "$out" name; gem spec "$out" version; exit 1; }
|
|
134
|
+
echo "$g" >> dist/ORDER
|
|
135
|
+
done
|
|
136
|
+
echo "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH (ruby $(ruby -e 'print RUBY_VERSION'), rubygems $(gem -v))"
|
|
137
|
+
(cd dist && sha256sum *.gem > SHA256SUMS && cat ORDER SHA256SUMS)
|
|
138
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
139
|
+
with:
|
|
140
|
+
name: gems-${{ steps.check.outputs.version }}
|
|
141
|
+
path: dist/
|
|
142
|
+
if-no-files-found: error
|
|
143
|
+
|
|
144
|
+
publish:
|
|
145
|
+
needs: build
|
|
146
|
+
runs-on: ubuntu-latest
|
|
147
|
+
environment: release
|
|
148
|
+
permissions:
|
|
149
|
+
contents: write # creates the tag on a dispatched release
|
|
150
|
+
id-token: write # the RubyGems trusted-publishing exchange
|
|
151
|
+
steps:
|
|
152
|
+
- name: Refuse a stale run (the tag is somewhere else)
|
|
153
|
+
shell: bash -eo pipefail {0}
|
|
154
|
+
env:
|
|
155
|
+
TAG: ${{ needs.build.outputs.tag }}
|
|
156
|
+
run: |
|
|
157
|
+
tagged=$(git ls-remote "https://github.com/$GITHUB_REPOSITORY" "refs/tags/$TAG*" |
|
|
158
|
+
awk -v t="refs/tags/$TAG" '$2 == t || $2 == t "^{}" { print $1 }')
|
|
159
|
+
if [ -z "$tagged" ] && [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then
|
|
160
|
+
echo "tag $TAG will be created at $GITHUB_SHA"
|
|
161
|
+
else
|
|
162
|
+
echo "$tagged" | grep -qx "$GITHUB_SHA" || { echo "::error::tag $TAG is not at $GITHUB_SHA (now: ${tagged:-gone}); this run is stale"; exit 1; }
|
|
163
|
+
fi
|
|
164
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
165
|
+
with:
|
|
166
|
+
name: gems-${{ needs.build.outputs.version }}
|
|
167
|
+
path: dist
|
|
168
|
+
- name: Check the sums
|
|
169
|
+
working-directory: dist
|
|
170
|
+
run: sha256sum -c SHA256SUMS
|
|
171
|
+
- uses: ruby/setup-ruby@de492cf36cbab201753d720ce4f4e336cd688c94 # v1.336.0
|
|
172
|
+
with:
|
|
173
|
+
ruby-version: '3.3'
|
|
174
|
+
- uses: rubygems/configure-rubygems-credentials@dc5a8d8553e6ee01fc26761a49e99e733d17954a # v2.1.0
|
|
175
|
+
- name: Dry run ends here
|
|
176
|
+
if: inputs.dry_run
|
|
177
|
+
run: echo "Credentials exchange succeeded, so the trusted publisher works. No tag created, nothing pushed."
|
|
178
|
+
- name: Push the gems
|
|
179
|
+
if: ${{ !inputs.dry_run }}
|
|
180
|
+
shell: bash -eo pipefail {0}
|
|
181
|
+
working-directory: dist
|
|
182
|
+
env:
|
|
183
|
+
VERSION: ${{ needs.build.outputs.version }}
|
|
184
|
+
run: |
|
|
185
|
+
while read -r g; do
|
|
186
|
+
sum=$(sha256sum "$g-$VERSION.gem" | cut -d' ' -f1)
|
|
187
|
+
published=$(curl -fsS "https://rubygems.org/api/v1/versions/$g.json" 2>/dev/null |
|
|
188
|
+
ruby -rjson -e 'b = $stdin.read; v = (b.empty? ? [] : JSON.parse(b)).find { |x| x["number"] == ENV["VERSION"] && x["platform"] == "ruby" }; print v ? v["sha"].to_s : ""' || true)
|
|
189
|
+
if [ -z "$published" ]; then
|
|
190
|
+
gem push "$g-$VERSION.gem" </dev/null
|
|
191
|
+
elif [ "$published" = "$sum" ]; then
|
|
192
|
+
echo "$g $VERSION is already on rubygems.org with this sum; skipping"
|
|
193
|
+
else
|
|
194
|
+
echo "::error::$g $VERSION is already on rubygems.org with sha256 $published, not this build's $sum"; exit 1
|
|
195
|
+
fi
|
|
196
|
+
done < ORDER
|
|
197
|
+
- name: Create the tag
|
|
198
|
+
if: github.event_name == 'workflow_dispatch' && !inputs.dry_run
|
|
199
|
+
shell: bash -eo pipefail {0}
|
|
200
|
+
env:
|
|
201
|
+
TAG: ${{ needs.build.outputs.tag }}
|
|
202
|
+
GH_TOKEN: ${{ github.token }}
|
|
203
|
+
run: |
|
|
204
|
+
tagged=$(git ls-remote "https://github.com/$GITHUB_REPOSITORY" "refs/tags/$TAG*" |
|
|
205
|
+
awk -v t="refs/tags/$TAG" '$2 == t || $2 == t "^{}" { print $1 }')
|
|
206
|
+
if [ -z "$tagged" ]; then
|
|
207
|
+
gh api "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$TAG" -f sha="$GITHUB_SHA"
|
|
208
|
+
elif echo "$tagged" | grep -qx "$GITHUB_SHA"; then
|
|
209
|
+
echo "tag $TAG is already at $GITHUB_SHA"
|
|
210
|
+
else
|
|
211
|
+
echo "::error::tag $TAG is not at $GITHUB_SHA (now: $tagged)"; exit 1
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
release-page:
|
|
215
|
+
needs: [build, publish]
|
|
216
|
+
if: ${{ !inputs.dry_run }}
|
|
217
|
+
permissions:
|
|
218
|
+
contents: write # github-release.yml creates the release
|
|
219
|
+
uses: ./.github/workflows/github-release.yml
|
|
220
|
+
with:
|
|
221
|
+
tag: ${{ needs.build.outputs.tag }}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## Version 1.8.0 (Oct 31 2026)
|
|
4
|
+
|
|
5
|
+
* Fix ThreadsOnFork and Forking children crashing on Ruby 3.3 with `[BUG] lazy sweeping underway when freeing object space` when they stopped on a `TERM` or `INT`, on an error, or when a job called `exit`, if a thread in the parent was running finalizers when the child was forked (an interpreter bug fixed in Ruby 3.4). On Ruby 3.3 a forked child now disables the GC as it exits, so its `at_exit` handlers (the app's, and those it inherited from the parent) run with the GC off, and a child running several threads stops its job threads before those handlers run (a job they were working on goes back to the queue and runs again). Other Rubies are unchanged. **What to do:** nothing; on Ruby 3.3 an `at_exit` handler that allocates a lot in a child now grows the child's memory until it exits
|
|
6
|
+
* Fix a delay of a fraction of a second being cut short: `delay` is now rounded up to whole seconds instead of down, so a job never becomes ready before its delay is up. Before, `delay: 0.5` enqueued a job that was ready at once, and an Active Job `set(wait: 1)` or `set(wait_until: ...)`, whose delay is always a little under the time asked for once it reaches the adapter, ran up to a second early (`wait: 1` ran at once). Whole-second delays are unchanged; a fractional one now waits up to a second longer than asked, never less. A fractional retry delay (a `retry_delay_proc` that returns a Float, or a Float `config.retry_delay`) is rounded up the same way; before, beanstalkd refused it (`Beaneater::BadFormatError`), which stopped the worker (the simple worker exited, and a ThreadsOnFork or Forking child died) instead of retrying the job. A fractional TTR (`:ttr` when enqueueing, a class's `queue_respond_timeout` or `config.respond_timeout`) is rounded up to whole seconds too; a Float `config.respond_timeout` used to fail every enqueue (`Beaneater::BadFormatError`). **Behaviour change:** a Float `:ttr`, `queue_respond_timeout` or `queue_retry_delay` (such as `queue_retry_delay 1.5`) used to be ignored in favour of the global default (`config.respond_timeout`, `config.retry_delay`); it is now used, rounded up. The same goes for a fractional `ActiveSupport::Duration` or Rational (`queue_respond_timeout 1.5.minutes` now gives a TTR of 90 instead of the default). If you set one by mistake, remove it to keep the default. `on_retry` hooks now get the rounded delay, and a `retry_delay_proc` that returns `nil`, a String that isn't a number, or a negative number now retries at once (delay 0) instead of stopping the worker with `Beaneater::BadFormatError`; a `:ttr`, `queue_respond_timeout` or `queue_retry_delay` that isn't finite (`Float::INFINITY`, `NaN`) is still ignored in favour of the default
|
|
7
|
+
* Fix a ThreadsOnFork child running more than one thread (`threads_number` or a tube's threads limit above 1) with `garbage_after` set stopping after its first `garbage_after` jobs on Ruby 3.2 and newer: once its threads had worked them, it waited for every thread in the process to finish, including the one the `timeout` gem keeps running since Ruby 3.2 (`timeout` 0.3), so it never exited to be forked again and the tube's jobs piled up. It now waits only for its own threads, so a thread a job leaves running no longer holds it up either. Ruby 3.1 and older with their bundled `timeout` weren't affected. **What to do:** nothing; a worker stuck this way recovers once upgraded and restarted
|
|
8
|
+
* Fix the usage comment in `bin/backburner`: queues are given with `-q` (`backburner -q foo,bar`); a bare `backburner foo,bar` ignores them and works every known queue
|
|
9
|
+
* Fix the ThreadsOnFork worker logging "Catastrophic failure: tube ... exited with code 0" for every child on a normal shutdown: while the worker shuts down, a child that exits with code 0 is now logged as "Tube ... exited normally.", and one that exits with another code or is killed (by the shutdown's `SIGKILL`) as "Tube ... stopped.", without being forked again. A child that exits with code 0 at another time (a signal sent to the child alone, or a job that called `exit`) is logged as a warning, "Tube ... exited with code 0. Forking it again in Ns."
|
|
10
|
+
* Fix an error raised by `config.on_error` (an error reporter that can't reach its service, say) stopping the worker: it escaped `work_one_job`, which ended the simple worker and killed a ThreadsOnFork or Forking child; it is now logged ("The on_error handler failed for ..."). The failed job is still retried or buried as before
|
|
11
|
+
* Fix the ThreadsOnFork worker forking children in a busy loop when they fail at once (hundreds a second while beanstalkd is down, each logging a "Catastrophic failure"): after a child fails, the parent now waits 1 second before forking the next one, doubling up to 30 seconds while children keep failing, and starts over once a child is recycled or runs for 30 seconds; recycling a child after `garbage_after` jobs is still immediate. A child running several threads now exits with code 1 (instead of 99) when one of its threads died of an error, so it backs off too
|
|
12
|
+
* Fix a job whose class can't be found or fails to load stopping the worker: the `Backburner::Job::JobNotFound` raised while handling its failure escaped `work_one_job` (so the simple worker exited and a ThreadsOnFork child died and was forked again in a busy loop), and so did any error other than a `NameError` raised while its class's file loaded (a `LoadError`, `KeyError`, `SyntaxError` and so on). When the class is missing (renamed or removed, or enqueued by code deployed before the workers: there is no such constant, or its autoload file isn't there), the worker now logs a warning ("... class not found; released for Ns") and releases the job with a delay of its TTR (at most 60 seconds), so a worker that has the class picks it up; it is never buried for this and doesn't use up its retries (beanstalkd still counts the releases, so a later failure gets fewer retries), and `on_error` and the failure hooks aren't called for it, as before. **What to do:** jobs of a class you removed on purpose now keep coming back, with a warning each time, instead of stopping the worker: delete them (with a beanstalkd client, say) or keep a stub class whose `perform` does nothing until they are drained. When the class's file fails to load (it raises a `NameError` for another constant, `NoMethodError`, `ArgumentError` or any other error, has a syntax error, or requires a missing gem), or the job names something that can't be a class (`"not a class"`), the job fails like any other: it is retried and then buried, `on_error` gets the `JobNotFound`, and its message names the cause on one line (`JobNotFound: SomeJob (LoadError: cannot load such file -- some_gem)`). **What to do:** fix the file and deploy, then kick the buried jobs
|
|
13
|
+
* Fix `handle_asynchronously` and `handle_static_asynchronously` wiping the class's queue settings: its jobs went to the primary queue (`backburner-jobs`) with the default priority and TTR instead of the class's `queue`, `queue_priority` and `queue_respond_timeout`. Including `Backburner::Performable` in a class that already includes `Backburner::Queue` no longer resets the settings it set either. **Behaviour change:** jobs of a class that calls `handle_asynchronously`/`handle_static_asynchronously` (including its `async` calls) now use the class's `queue`, `queue_priority` and `queue_respond_timeout` (before, the settings declared ahead of the call were lost). Workers started with `-q` or `default_queues` must also watch the class's tube (a worker started with no queues already does). Jobs enqueued before the upgrade stay on the primary queue, so keep a worker on it until they are drained
|
|
14
|
+
* Fix apps on Rails 6.1, 7.0 and 7.1 failing to boot with backburner 1.7.0 (`uninitialized constant ActiveJob::QueueAdapters::AbstractAdapter` from `require 'backburner'`, or from `config.active_job.queue_adapter = :backburner`): the gem's Active Job adapter subclasses `AbstractAdapter` only on Rails 7.2+, which added it, and `require 'backburner'` loads it only there; older Rails autoload the adapter as before. Where the gem's adapter loads in place of Rails' own (whenever the gem's lib comes first on the load path, as Bundler puts it), it also sets `provider_job_id` on Rails 6.1 and 7.0. The adapter now requires backburner itself, as Rails' own adapter did, so apps with `gem 'backburner', require: false` can enqueue again (since 1.7.0 the first enqueue raised `uninitialized constant Backburner::Worker`). CI now also runs the suite with Active Job 6.1
|
|
15
|
+
* Fix a worker started without queues (`backburner` with no `-q`) never working the tubes it found on the server when the namespace has capitals (`MyApp`), an underscore or regexp characters (`jobs(eu)`, `app+`), when the separator isn't `.`, or for queue names such as ActiveJob's `production_mailers` under the namespace `production` (tube `production.-mailers`) and `report_2026`: it expanded those tube names again and watched tubes nobody enqueues on (`MyApp.my-app.default` instead of `MyApp.default`). It now watches the server's tubes this configuration could have produced (they start with the namespace and separator, and the rest has no capitals or underscores), under their own names, and a found tube with `--` in its name (queue `a__b`) no longer stops it at start with `NoMethodError`. The tube names jobs are enqueued on don't change
|
|
16
|
+
* Fix a `tube_namespace` that isn't a valid regexp (such as `app(`) failing every enqueue and stopping workers with `RegexpError`
|
|
17
|
+
* Fix `namespace_separator = ''` failing every enqueue with `RegexpError`: the namespace and the queue name are now joined directly (`appdefault`, or `app.default` for the namespace `app.`)
|
|
18
|
+
* Fix a failing job stopping the worker when it runs in a directory whose name has regexp characters (such as `jobs[`): logging the error raised `RegexpError`
|
|
19
|
+
* Fix ThreadsOnFork with one thread per tube: each forked child now opens its own beanstalkd connection instead of sharing the parent's socket, which mixed up replies between children (`undefined method 'ttr' for nil`, lost jobs) (reported by @Haerezis in #169 and @supremoUL in #175)
|
|
20
|
+
* Fix workers never recovering after beanstalkd is down for more than a few seconds: a failed reconnect no longer leaves the connection unusable (`undefined method 'tubes'` in a busy loop), and the simple worker watches its tubes again after reconnecting; a job that fails while the connection is down no longer stops the simple worker either. A job that finishes while beanstalkd can't be reached (past beaneater's reconnect attempts) can't be deleted: `on_error` gets the `Beaneater::NotConnected`, and beanstalkd hands the job out again if it still has it (reported by @activestylus in #137)
|
|
21
|
+
* Fix `Connection#retryable` raising `NameError` instead of `Beaneater::NotConnected` once its retries are used up
|
|
22
|
+
* Fix a job that outlives its TTR stopping the worker: when beanstalkd has already taken the job back, the `NOT_FOUND` from the retry or bury is now logged instead of escaping `work_one_job` (reported by @boncey in #82)
|
|
23
|
+
* Behaviour change: a `before_perform` hook returning false now deletes the job, as HOOKS.md describes; before, the job stayed reserved and beanstalkd handed it out again after every TTR (reported by @inoce1e in #145)
|
|
24
|
+
* Fix docs: a TTR of 0 does not disable the timeout, beanstalkd stores it as 1 (reported by @pnabutovsky in #126); the HOOKS.md failure-hook examples no longer call `self.bury`, which raised `NoMethodError` and hid the original error (reported by @contentfree in #172); the README no longer points to the removed `retry_connection!` (reported by @att14 in #62)
|
|
25
|
+
* Fix queue settings not being inherited: a subclass of a `Backburner::Queue` job class now uses its parent's `queue`, `queue_priority`, `queue_respond_timeout`, `queue_max_job_retries`, `queue_retry_delay`, `queue_retry_delay_proc`, `queue_jobs_limit`, `queue_garbage_limit` and `queue_retry_limit` unless it sets its own, and is registered in `Backburner::Worker.known_queue_classes` (anonymous classes, such as `Class.new(SomeJob)`, are left out when workers list the known queues). For the Threading and ThreadsOnFork workers, a tube's `queue_jobs_limit`, `queue_garbage_limit` and `queue_retry_limit` come from the class that declares the tube; subclasses that share it only fill in the limits it leaves unset. **Behaviour change:** a subclass that didn't set `queue` used to enqueue on the primary queue (`backburner-jobs`); it now enqueues on its parent's queue (tube). Jobs it enqueued before the upgrade stay on the primary queue, so keep a worker on that tube until they are drained. A subclass can't unset an inherited setting: the setters ignore `nil` and `false` (reported by @contentfree in #141)
|
|
26
|
+
* Fix every enqueue opening a new TCP connection to beanstalkd (closed sockets piled up in `TIME_WAIT` until enqueueing failed with `Beaneater::NotConnected`): enqueueing reuses pooled connections (up to 5 idle per process, each reused for up to `config.enqueue_connection_max_idle` seconds, default 60), a forked child opens its own, and a connection that an error interrupted is closed rather than reused. While beanstalkd is down, each enqueue that picks up an idle pooled connection (up to 5) now takes about 2 seconds of retries before raising `Beaneater::NotConnected`, instead of failing at once; later ones fail at once (reported by @xinuc in #125, confirmed by @funkyboy)
|
|
27
|
+
* Allow `config.tube_namespace = nil` (or `false`) for tubes without a prefix: jobs go to `backburner-jobs`, `newsletter-sender` and so on. `nil` and `false` used to raise `NoMethodError`; `''` still gives tube names that start with the separator (`.backburner-jobs`), as before. With no namespace, a worker started without tubes watches every tube on the server without capitals or underscores (including `default`), so give your tubes distinct names on a shared beanstalkd (@forresty in #94)
|
|
28
|
+
* Fix `config.default_priority` set to a priority label (such as `:high`) failing every enqueue with `Beaneater::BadFormatError`: the label now resolves like a job's priority label (reported by @rewritten in #52, tracked in #54)
|
|
29
|
+
* Fix `require 'backburner'` on Ruby 4.0 in a bundle without ActiveSupport (`cannot load such file -- logger`): `logger` is no longer a default gem, so the gemspec now depends on it
|
|
30
|
+
* Require Ruby 2.7 or newer (the gemspec now says so; CI covers 2.7 to 4.0)
|
|
31
|
+
|
|
3
32
|
## Version 1.7.0 (Jan 2 2026)
|
|
4
33
|
|
|
5
34
|
* Changes to ActiveJob Rails adapter to remove default adapter and associated tests
|
data/Gemfile
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
source 'https://rubygems.org'
|
|
2
2
|
|
|
3
3
|
# Specify your gem's dependencies in backburner.gemspec
|
|
4
|
-
gemspec
|
|
4
|
+
gemspec
|
|
5
|
+
|
|
6
|
+
# Test against an older Active Job (and the Active Support it needs), as CI does: ACTIVEJOB_VERSION=7.1 bundle install
|
|
7
|
+
gem 'activejob', "~> #{ENV['ACTIVEJOB_VERSION']}.0" unless ENV['ACTIVEJOB_VERSION'].to_s.empty?
|
data/HOOKS.md
CHANGED
|
@@ -17,7 +17,7 @@ There are a variety of hooks available that are triggered during the lifecycle o
|
|
|
17
17
|
Any exception raised propagates up to the code which queued the job.
|
|
18
18
|
|
|
19
19
|
* `before_perform`: Called with the job args before perform. If a hook returns false,
|
|
20
|
-
the job is aborted. Other exceptions are treated like regular job exceptions.
|
|
20
|
+
the job is aborted and deleted from the queue. Other exceptions are treated like regular job exceptions.
|
|
21
21
|
|
|
22
22
|
* `after_perform`: Called with the job args after it performs. Uncaught
|
|
23
23
|
exceptions will be treated like regular job exceptions.
|
|
@@ -41,9 +41,8 @@ class SomeJob
|
|
|
41
41
|
logger.info "About to perform #{self} with #{args.inspect}"
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def self.
|
|
44
|
+
def self.on_failure_log_job(e, *args)
|
|
45
45
|
logger.info "Performing #{self} caused an exception (#{e})"
|
|
46
|
-
self.bury
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
def self.perform(*args)
|
|
@@ -56,25 +55,27 @@ class SomeJob
|
|
|
56
55
|
end
|
|
57
56
|
```
|
|
58
57
|
|
|
58
|
+
A failure hook doesn't need to retry or bury the job: the worker retries a failed job up to `max_job_retries`
|
|
59
|
+
times and then buries it. Use an `on_retry` or `on_bury` hook to act on those.
|
|
60
|
+
|
|
59
61
|
You can also setup modules to create compose-able and reusable hooks for your jobs. For example:
|
|
60
62
|
|
|
61
63
|
```ruby
|
|
62
64
|
module LoggedJob
|
|
63
65
|
def before_perform_log_job(*args)
|
|
64
|
-
|
|
66
|
+
puts "About to perform #{self} with #{args.inspect}"
|
|
65
67
|
end
|
|
66
68
|
end
|
|
67
69
|
|
|
68
|
-
module
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
self.bury
|
|
70
|
+
module NotifiedJob
|
|
71
|
+
def on_failure_notify(e, *args)
|
|
72
|
+
puts "Performing #{self} caused an exception (#{e})"
|
|
72
73
|
end
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
class MyJob
|
|
76
77
|
extend LoggedJob
|
|
77
|
-
extend
|
|
78
|
+
extend NotifiedJob
|
|
78
79
|
|
|
79
80
|
def self.perform(*args)
|
|
80
81
|
# ...
|
|
@@ -86,14 +87,22 @@ end
|
|
|
86
87
|
|
|
87
88
|
Currently, there is just one hook:
|
|
88
89
|
|
|
89
|
-
* `on_reconnect`: Called on
|
|
90
|
-
|
|
90
|
+
* `on_reconnect`: Called on a Simple or ThreadsOnFork worker after Backburner re-establishes
|
|
91
|
+
a lost connection, with the new connection as the argument. A short outage that beanstalk's
|
|
92
|
+
client library rides out on its own reconnects without it (and keeps the watched tubes). The
|
|
93
|
+
Threading worker never calls it (each thread's connection re-watches its own tube), and
|
|
94
|
+
Forking workers' children reconnect through the client library, so it doesn't fire there either.
|
|
95
|
+
Every public method whose name starts with `on_reconnect` is called, so add your own as
|
|
96
|
+
`on_reconnect_<something>`. The Simple, Forking and ThreadsOnFork
|
|
97
|
+
workers define `on_reconnect` itself to watch their tubes again; if you override
|
|
98
|
+
`on_reconnect` in a subclass of one of them, call `super`.
|
|
91
99
|
|
|
92
100
|
An example:
|
|
93
101
|
|
|
94
102
|
```ruby
|
|
95
|
-
class MyWorker < Backburner::
|
|
96
|
-
def
|
|
97
|
-
|
|
103
|
+
class MyWorker < Backburner::Workers::Simple
|
|
104
|
+
def on_reconnect_log(conn)
|
|
105
|
+
log_info "reconnected to beanstalkd"
|
|
98
106
|
end
|
|
99
107
|
end
|
|
108
|
+
```
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Backburner [](https://github.com/nesquena/backburner/actions/workflows/ci.yml)
|
|
2
2
|
|
|
3
3
|
Backburner is a [beanstalkd](https://beanstalkd.github.io/)-powered job queue that can handle a very high volume of jobs.
|
|
4
4
|
You create background jobs and place them on multiple work queues to be processed later.
|
|
@@ -113,14 +113,14 @@ The key options available are:
|
|
|
113
113
|
| Option | Description |
|
|
114
114
|
| ----------------- | ------------------------------- |
|
|
115
115
|
| `beanstalk_url` | Address for beanstalkd connection i.e 'beanstalk://127.0.0.1' |
|
|
116
|
-
| `tube_namespace` | Prefix used for all tubes related to this backburner queue.
|
|
116
|
+
| `tube_namespace` | Prefix used for all tubes related to this backburner queue (`nil` or `false` for no prefix; a worker given no tubes then watches every tube on the server without capitals or underscores, so give your tubes distinct names on a shared beanstalkd; `''` gives tube names that start with the separator, such as `.backburner-jobs`, as before). |
|
|
117
117
|
| `namespace_separator` | Separator used for namespace and queue name |
|
|
118
118
|
| `on_error` | Lambda invoked with the error whenever any job in the system fails. |
|
|
119
119
|
| `max_job_retries` | Integer defines how many times to retry a job before burying. |
|
|
120
|
-
| `retry_delay` |
|
|
120
|
+
| `retry_delay` | Number defines the base time to wait (in secs) between job retries. |
|
|
121
121
|
| `retry_delay_proc` | Lambda calculates the delay used, allowing for exponential back-off. |
|
|
122
|
-
| `default_priority` | Integer
|
|
123
|
-
| `respond_timeout` |
|
|
122
|
+
| `default_priority` | Integer or priority label: the default priority of jobs |
|
|
123
|
+
| `respond_timeout` | Number defines how long a job has to complete its task (in secs) |
|
|
124
124
|
| `default_worker` | Worker class that will be used if no other worker is specified. |
|
|
125
125
|
| `logger` | Logger recorded to when backburner wants to report info or errors. |
|
|
126
126
|
| `primary_queue` | Primary queue used for a job when an alternate queue is not given. |
|
|
@@ -128,6 +128,17 @@ The key options available are:
|
|
|
128
128
|
| `reserve_timeout` | Duration to wait for work from a single server, or nil for forever. |
|
|
129
129
|
| `job_serializer_proc` | Lambda serializes a job body to a string to write to the task |
|
|
130
130
|
| `job_parser_proc` | Lambda parses a task body string to a hash |
|
|
131
|
+
| `enqueue_connection_max_idle` | Seconds an idle enqueue connection is reused for before it is replaced (default 60; `nil` for no limit; 0 or less opens a new connection for every enqueue). |
|
|
132
|
+
|
|
133
|
+
A tube's name is the namespace, the separator and the queue name, dasherized (`some.app.production.newsletter-sender`).
|
|
134
|
+
Don't start queue names with the namespace: that part of the queue name is dropped, so with the namespace `app` the queues
|
|
135
|
+
`mail` and `appmail` share the tube `app.mail` and `apple-sync` uses `app.le-sync` (an ActiveJob `queue_name_prefix` equal
|
|
136
|
+
to the namespace does the same: `production_mailers` uses `production.-mailers`). It stays that way because changing it
|
|
137
|
+
would leave queued jobs in the old tubes. A worker given no tubes watches the tubes on the server whose names start with the
|
|
138
|
+
namespace and separator and whose rest has no capitals or underscores (the names this configuration could have produced),
|
|
139
|
+
the tubes of the job classes it has loaded and the primary queue. On a shared beanstalkd, don't give one app a namespace
|
|
140
|
+
that starts with another's namespace and separator (`app` and `app.prod`, `MyApp` and `MyApp.admin`): workers of the
|
|
141
|
+
shorter one given no tubes reserve the other's jobs, which they can't run.
|
|
131
142
|
|
|
132
143
|
## Breaking Changes
|
|
133
144
|
|
|
@@ -165,7 +176,7 @@ class NewsletterJob
|
|
|
165
176
|
|
|
166
177
|
# optional, defaults to respond_timeout in config
|
|
167
178
|
def self.queue_respond_timeout
|
|
168
|
-
300 # number of seconds before job times out
|
|
179
|
+
300 # number of seconds before job times out; the minimum is 1 (beanstalkd turns 0 into 1), so give long jobs a generous value. NB: A timeout of 1 second will likely lead to race conditions between Backburner and beanstalkd and should be avoided
|
|
169
180
|
end
|
|
170
181
|
|
|
171
182
|
# optional, defaults to retry_delay_proc in config
|
|
@@ -192,7 +203,7 @@ class NewsletterJob
|
|
|
192
203
|
include Backburner::Queue
|
|
193
204
|
queue "newsletter-sender" # defaults to 'backburner-jobs' tube
|
|
194
205
|
queue_priority 1000 # most urgent priority is 0
|
|
195
|
-
queue_respond_timeout 300 # number of seconds before job times out
|
|
206
|
+
queue_respond_timeout 300 # number of seconds before job times out (minimum 1; 0 does not disable it)
|
|
196
207
|
|
|
197
208
|
def self.perform(email, body)
|
|
198
209
|
NewsletterMailer.deliver_text_to_email(email, body)
|
|
@@ -200,6 +211,11 @@ class NewsletterJob
|
|
|
200
211
|
end
|
|
201
212
|
```
|
|
202
213
|
|
|
214
|
+
A subclass of such a job uses its parent's settings (including its tube) unless it sets its own.
|
|
215
|
+
It can't unset one: the setters ignore `nil` and `false`. For the Threading and ThreadsOnFork workers, a shared
|
|
216
|
+
tube's limits (`queue_jobs_limit`, `queue_garbage_limit`, `queue_retry_limit`) come from the class that declares the
|
|
217
|
+
tube; subclasses only fill in the limits it leaves unset.
|
|
218
|
+
|
|
203
219
|
Jobs can be enqueued with:
|
|
204
220
|
|
|
205
221
|
```ruby
|
|
@@ -224,7 +240,7 @@ class User
|
|
|
224
240
|
include Backburner::Performable
|
|
225
241
|
queue "user-jobs" # defaults to 'user'
|
|
226
242
|
queue_priority 500 # most urgent priority is 0
|
|
227
|
-
queue_respond_timeout 300 # number of seconds before job times out
|
|
243
|
+
queue_respond_timeout 300 # number of seconds before job times out (minimum 1; 0 does not disable it)
|
|
228
244
|
|
|
229
245
|
def activate(device_id)
|
|
230
246
|
@device = Device.find(device_id)
|
|
@@ -408,6 +424,8 @@ Backburner::Worker.enqueue NewsletterJob, ["foo", "bar"], :pri => :custom
|
|
|
408
424
|
User.async(:pri => :useful, :delay => 10.seconds).reset_password(@user.id)
|
|
409
425
|
```
|
|
410
426
|
|
|
427
|
+
They work for `config.default_priority` too (e.g. `config.default_priority = :custom`).
|
|
428
|
+
|
|
411
429
|
Using named priorities can greatly simplify priority management.
|
|
412
430
|
|
|
413
431
|
### Processing Strategies
|
|
@@ -611,7 +629,12 @@ backburner -e $environment
|
|
|
611
629
|
|
|
612
630
|
In Backburner, if the beanstalkd connection is temporarily severed, several retries to establish the connection will be attempted.
|
|
613
631
|
After several retries, if the connection is still not able to be made, a `Beaneater::NotConnected` exception will be raised.
|
|
614
|
-
|
|
632
|
+
Workers log the exception and try again on their next reserve, and watch their tubes again once the connection is back.
|
|
633
|
+
Elsewhere (when enqueuing, for example) you can catch the exception and retry the call yourself.
|
|
634
|
+
|
|
635
|
+
Enqueueing reuses a small pool of connections (up to 5 idle per process). While beanstalkd is down, each enqueue
|
|
636
|
+
that picks up one of those idle connections takes about 2 seconds of retries before raising `Beaneater::NotConnected`;
|
|
637
|
+
once they are used up, enqueues fail at once until beanstalkd is back.
|
|
615
638
|
|
|
616
639
|
### Web Front-end
|
|
617
640
|
|
data/backburner.gemspec
CHANGED
|
@@ -15,13 +15,16 @@ Gem::Specification.new do |s|
|
|
|
15
15
|
s.require_paths = ["lib"]
|
|
16
16
|
s.version = Backburner::VERSION
|
|
17
17
|
s.license = 'MIT'
|
|
18
|
+
s.required_ruby_version = '>= 2.7'
|
|
18
19
|
|
|
19
20
|
s.add_runtime_dependency 'beaneater', '~> 1.0'
|
|
20
21
|
s.add_runtime_dependency 'dante', '> 0.1.5'
|
|
21
22
|
s.add_runtime_dependency 'concurrent-ruby', '~> 1.0', '>= 1.0.1'
|
|
23
|
+
s.add_runtime_dependency 'logger' # a bundled gem (no longer a default one) since Ruby 4.0
|
|
22
24
|
|
|
23
25
|
s.add_development_dependency 'rake'
|
|
24
26
|
s.add_development_dependency 'minitest', '5.1.0'
|
|
27
|
+
s.add_development_dependency 'mutex_m' # minitest 5.1.0 requires it; a bundled gem (not a default one) since Ruby 3.4
|
|
25
28
|
s.add_development_dependency 'mocha'
|
|
26
29
|
s.add_development_dependency 'activejob', '>= 6.0'
|
|
27
30
|
s.add_development_dependency 'activesupport', '>= 6.0'
|
data/bin/backburner
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
# Rails' own adapter files require backburner too, for apps that don't require it themselves
|
|
2
|
+
require 'backburner' unless defined?(Backburner::Worker)
|
|
3
|
+
|
|
1
4
|
module ActiveJob
|
|
2
5
|
module QueueAdapters
|
|
3
|
-
#
|
|
4
|
-
|
|
6
|
+
# Rails 7.2 added AbstractAdapter; the adapters of older Rails have no superclass
|
|
7
|
+
rails_7_2_or_newer = const_defined?(:AbstractAdapter, false)
|
|
8
|
+
|
|
9
|
+
# Explicitly remove the implementation rails' 7.2+ ship. Older rails' keep theirs, unless their autoload
|
|
10
|
+
# loads this file instead (it shadows theirs when this gem's lib comes first on the load path).
|
|
11
|
+
remove_const(:BackburnerAdapter) if rails_7_2_or_newer && const_defined?(:BackburnerAdapter)
|
|
5
12
|
|
|
6
13
|
# = Backburner adapter for Active Job
|
|
7
14
|
#
|
|
8
15
|
# To use Backburner set the queue_adapter config to +:backburner+.
|
|
9
16
|
#
|
|
10
17
|
# Rails.application.config.active_job.queue_adapter = :backburner
|
|
11
|
-
class BackburnerAdapter < ::ActiveJob::QueueAdapters::AbstractAdapter
|
|
18
|
+
class BackburnerAdapter < (rails_7_2_or_newer ? ::ActiveJob::QueueAdapters::AbstractAdapter : Object)
|
|
12
19
|
def enqueue(job)
|
|
13
20
|
response = Backburner::Worker.enqueue(JobWrapper, [job.serialize], queue: job.queue_name, pri: job.priority)
|
|
14
21
|
job.provider_job_id = response[:id] if response.is_a?(Hash)
|