├── .dockerignore ├── .github ├── contributing.md ├── issue_template.md ├── notes.rb ├── release-notes.md.erb └── workflows │ ├── ci.yml │ └── ent.yml ├── .gitignore ├── .golangci.yml ├── .local.sh ├── COMM-LICENSE ├── Changes.md ├── Dockerfile ├── Ent-Changes.md ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── cli ├── cli.go ├── cli_test.go ├── security_test.go └── test-fixtures │ └── case-one │ └── conf.d │ ├── a.toml │ └── b.toml ├── client ├── LICENSE ├── batch.go ├── client.go ├── client_bsd.go ├── client_linux.go ├── client_test.go ├── client_windows.go ├── faktory.go ├── job.go ├── job_test.go ├── mutate.go ├── package.go ├── pool.go ├── pool_test.go └── tracking.go ├── cmd ├── faktory-cli │ └── main.go └── faktory │ └── daemon.go ├── docs ├── commands-validity.rst ├── protocol-specification.md └── webui.png ├── example ├── config.toml ├── cron.png ├── cronloop.png ├── webui-throttle.png └── whitelabel.png ├── go.mod ├── go.sum ├── internal └── pool │ ├── pool.go │ └── pool_test.go ├── manager ├── fetch.go ├── manager.go ├── manager_test.go ├── middleware.go ├── middleware_test.go ├── retry.go ├── retry_test.go ├── scheduler.go ├── scheduler_test.go ├── working.go └── working_test.go ├── packaging ├── root │ └── usr │ │ └── share │ │ └── faktory │ │ ├── init │ │ ├── faktory.conf │ │ └── faktory.rpm.conf │ │ └── systemd │ │ └── faktory.service └── scripts │ ├── postinst.deb.systemd │ ├── postinst.deb.upstart │ ├── postinst.rpm.systemd │ ├── postinst.rpm.upstart │ ├── postrm.deb.systemd │ ├── postrm.deb.upstart │ ├── postrm.rpm.systemd │ ├── postrm.rpm.upstart │ ├── prerm.deb.systemd │ ├── prerm.deb.upstart │ ├── prerm.rpm.systemd │ └── prerm.rpm.upstart ├── server ├── commands.go ├── commands_test.go ├── config.go ├── connection.go ├── connection_test.go ├── mutate.go ├── mutate_test.go ├── scanner.go ├── server.go ├── server_test.go ├── subsystem.go ├── task_runner.go ├── tasks.go ├── workers.go └── workers_test.go ├── storage ├── history.go ├── history_test.go ├── queue_redis.go ├── queue_test.go ├── raw.go ├── redis.go ├── redis_test.go ├── sorted_redis.go ├── sorted_test.go └── types.go ├── test ├── auth │ └── password ├── cfg │ ├── private.key.pem │ └── public.cert.pem ├── ent │ └── batch_test.go ├── go_system_test.go ├── load │ └── main.go ├── ruby │ ├── Gemfile │ └── app.rb └── worker.rb ├── util ├── json.go ├── logger.go ├── util.go ├── util_bsd.go ├── util_linux.go ├── util_test.go └── util_windows.go └── webui ├── busy.ego ├── busy.ego.go ├── context.go ├── dead.ego ├── dead.ego.go ├── debug.ego ├── debug.ego.go ├── debugging.go ├── footer.ego ├── footer.ego.go ├── helpers.go ├── helpers_test.go ├── index.ego ├── index.ego.go ├── job_info.ego ├── job_info.ego.go ├── layout.ego ├── layout.ego.go ├── morgue.ego ├── morgue.ego.go ├── nav.ego ├── nav.ego.go ├── pages.go ├── pages_test.go ├── paging.ego ├── paging.ego.go ├── queue.ego ├── queue.ego.go ├── queues.ego ├── queues.ego.go ├── retries.ego ├── retries.ego.go ├── retry.ego ├── retry.ego.go ├── scheduled.ego ├── scheduled.ego.go ├── scheduled_job.ego ├── scheduled_job.ego.go ├── static ├── application-rtl.css ├── application.css ├── application.js ├── bootstrap-rtl.min.css ├── bootstrap.css ├── dashboard.js ├── img │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── favicon.svg │ └── status.png └── locales │ ├── ar.yml │ ├── cs.yml │ ├── da.yml │ ├── de.yml │ ├── el.yml │ ├── en.yml │ ├── es.yml │ ├── fa.yml │ ├── fr.yml │ ├── he.yml │ ├── hi.yml │ ├── it.yml │ ├── ja.yml │ ├── ko.yml │ ├── nb.yml │ ├── nl.yml │ ├── pl.yml │ ├── pt-br.yml │ ├── pt.yml │ ├── ru.yml │ ├── sv.yml │ ├── ta.yml │ ├── uk.yml │ ├── ur.yml │ ├── vi.yml │ ├── zh-cn.yml │ └── zh-tw.yml ├── summary.ego ├── summary.ego.go ├── timeago.go ├── web.go └── web_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | docker-compose.yml 3 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | 5 | Let us know how we can help! 6 | 7 | * include any **stack traces** with your error 8 | * list versions you are using: Faktory, workers, OS, etc. 9 | 10 | Better to include more info than less. 11 | 12 | ## Code 13 | 14 | It's always best to open an issue before investing a lot of time into a 15 | fix or new functionality. Functionality must meet my design goals and 16 | vision for the project to be accepted; I would be happy to discuss how 17 | your idea can best fit into Faktory. 18 | 19 | ## Legal 20 | 21 | By submitting a Pull Request, you disavow any rights or claims to any changes 22 | submitted to the Faktory project and assign the copyright of 23 | those changes to Contributed Systems LLC. 24 | 25 | If you cannot or do not want to reassign those rights (your employment 26 | contract for your employer may not allow this), you should not submit a PR. 27 | Open an issue and someone else can do the work. 28 | 29 | This is a legal way of saying "If you submit a PR to us, that code becomes ours". 30 | 99.9% of the time that's what you intend anyways; we hope it doesn't scare you 31 | away from contributing. 32 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | - Which Faktory package and version? 2 | - Which Faktory worker package and version? 3 | - Please include any relevant worker configuration 4 | - Please include any relevant error messages or stacktraces 5 | 6 | Are you using an old version? 7 | Have you checked the changelogs to see if your issue has been fixed in a later version? 8 | 9 | https://github.com/contribsys/faktory/blob/master/Changes.md 10 | https://github.com/contribsys/faktory/blob/master/Pro-Changes.md 11 | https://github.com/contribsys/faktory/blob/master/Ent-Changes.md 12 | -------------------------------------------------------------------------------- /.github/notes.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | require 'shellwords' 3 | 4 | # GitHub release notes auto-generator 5 | # Use like `ruby notes.rb 0.9.0` 6 | raise "invalid arguments" unless ARGV.size == 1 7 | bigver = ARGV[0] 8 | shortver = bigver.gsub(/[^0-9]/, "") 9 | title = bigver 10 | sums = {} 11 | 12 | Dir["packaging/output/systemd/*"].each do |fullname| 13 | name = File.basename(fullname) 14 | 15 | output = `shasum -a 256 #{Shellwords.escape(fullname)}` 16 | if $?.exitstatus != 0 17 | raise output 18 | end 19 | sums[name] = output.split[0] 20 | end 21 | 22 | content = ERB.new(File.read("#{__dir__}/release-notes.md.erb"), trim_mode: '-').result(binding) 23 | File.open("/tmp/release-notes.md", "w") do |file| 24 | file.write(content) 25 | end 26 | -------------------------------------------------------------------------------- /.github/release-notes.md.erb: -------------------------------------------------------------------------------- 1 | <%= title %> 2 | 3 | Notable changes can be found in the changelogs: 4 | 5 | * [Faktory](https://github.com/contribsys/faktory/blob/master/Changes.md#<%= shortver %>) 6 | * [Faktory Enterprise](https://github.com/contribsys/faktory/blob/master/Ent-Changes.md#<%= shortver %>) 7 | 8 | | Filename | SHA256 | 9 | | --- | --- | 10 | <% sums.keys.grep_v(/faktory-[ep]/).sort.each do |filename| -%> 11 | | <%= filename %> | <%= sums[filename] %> | 12 | <% end -%> 13 | <% sums.keys.grep(/faktory-[ep]/).sort.each do |filename| -%> 14 | | <%= filename %> | <%= sums[filename] %> | 15 | <% end -%> 16 | 17 | Verify with `shasum -a 256 `. 18 | 19 | [Installation](https://github.com/contribsys/faktory/wiki/Installation) | [Full docs](https://github.com/contribsys/faktory/wiki/) 20 | 21 | The Faktory Enterprise macOS packages are bare binaries which can replace the `faktory` binary installed by Homebrew. You are welcome to use either to trial the commercial functionality before purchase but they may not be used in a `production` environment without a license. Linux DEB packages and Docker images are available upon [purchase](https://contribsys.com/faktory). 22 | 23 | Unpack them with `tar xvf faktory-ent-*.tbz; cp ./faktory /usr/local/bin`. 24 | 25 | Note the `amd64` builds are for x86_64 processors (Intel, AMD). `arm64` builds are for ARM (Gravitron, Apple Silicon, etc) processors. 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install Redis 17 | run: sudo apt-get install redis-server 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: "1.24" 23 | 24 | - name: Prepare 25 | run: make prepare 26 | 27 | - name: Test 28 | run: make test 29 | -------------------------------------------------------------------------------- /.github/workflows/ent.yml: -------------------------------------------------------------------------------- 1 | # This is a CI workflow that runs the test against Enterprise Edition of Faktory. 2 | # The binary (for macos only) is avalable for download for testing purposes with each Faktory release. 3 | permissions: 4 | contents: read 5 | on: 6 | push: 7 | # branches: 8 | # - main 9 | pull_request: 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 12 | cancel-in-progress: true 13 | name: enterprise 14 | jobs: 15 | test: 16 | runs-on: macos-latest 17 | env: 18 | GO_VERSION: "1.22" 19 | FAKTORY_VERSION: 1.8.0 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Install redis 23 | run: brew install redis 24 | - name: Download Faktory binary 25 | run: | 26 | wget -O faktory.tbz https://github.com/contribsys/faktory/releases/download/v${{ env.FAKTORY_VERSION }}/faktory-ent_${{ env.FAKTORY_VERSION }}.macos.amd64.tbz 27 | tar xfv faktory.tbz 28 | cp ./faktory /usr/local/bin 29 | - name: Launch Faktory in background 30 | run: faktory & 31 | - name: Set up Go 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version: ${{ env.GO_VERSION }} 35 | - name: Prepare 36 | run: make prepare 37 | - name: Test 38 | env: 39 | FAKTORY_URL: tcp://127.0.0.1:7419 40 | FAKTORY_ENT: true 41 | run: | 42 | make clean 43 | make generate 44 | go test -parallel 4 ./test/ent 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | tmp/ 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 15 | .glide/ 16 | .vagrant 17 | packaging/output/ 18 | vendor/ 19 | 20 | .DS_Store 21 | .vscode 22 | 23 | /faktory 24 | /faktory_amd64 25 | /faktory_arm64 26 | /faktory-cli 27 | /loadtest 28 | /coverage.html 29 | tags 30 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - gocritic 5 | - gosec 6 | settings: 7 | gocritic: 8 | disabled-checks: 9 | - commentedOutCode 10 | - paramTypeCombine 11 | - ifElseChain 12 | - unnamedResult 13 | enabled-tags: 14 | - performance 15 | - diagnostic 16 | - opinionated 17 | settings: 18 | rangeExprCopy: 19 | sizeThreshold: 24 20 | rangeValCopy: 21 | sizeThreshold: 24 22 | exclusions: 23 | generated: lax 24 | presets: 25 | - comments 26 | - common-false-positives 27 | - legacy 28 | - std-error-handling 29 | paths: 30 | - third_party$ 31 | - builtin$ 32 | - examples$ 33 | formatters: 34 | exclusions: 35 | generated: lax 36 | paths: 37 | - third_party$ 38 | - builtin$ 39 | - examples$ 40 | -------------------------------------------------------------------------------- /.local.sh: -------------------------------------------------------------------------------- 1 | ROCKSDB_HOME=/usr/local/Cellar/rocksdb/5.5.1 2 | CGO_CFLAGS="-I${ROCKSDB_HOME}/include" 3 | CGO_LDFLAGS="-L${ROCKSDB_HOME} -lrocksdb -lstdc++ -lm -lz -lbz2 -lsnappy -llz4 -lzstd" 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 2 | ARG TARGETPLATFORM 3 | RUN apk add --no-cache redis ca-certificates socat 4 | COPY ./tmp/$TARGETPLATFORM /faktory 5 | 6 | RUN mkdir -p /root/.faktory/db 7 | RUN mkdir -p /.faktory/db 8 | RUN mkdir -p /var/lib/faktory/db 9 | RUN mkdir -p /etc/faktory 10 | 11 | EXPOSE 7419 7420 12 | 13 | RUN chgrp -R 0 /var/lib/faktory && \ 14 | chmod -R g=u /var/lib/faktory && \ 15 | chgrp -R 0 /.faktory/db && \ 16 | chmod -R g=u /.faktory/db && \ 17 | chgrp -R 0 /etc/faktory && \ 18 | chmod -R g=u /etc/faktory 19 | 20 | CMD ["/faktory", "-w", "0.0.0.0:7420", "-b", "0.0.0.0:7419"] 21 | -------------------------------------------------------------------------------- /Ent-Changes.md: -------------------------------------------------------------------------------- 1 | # Faktory Enterprise Changelog 2 | 3 | Changelog: [Faktory](https://github.com/contribsys/faktory/blob/master/Changes.md) || Faktory Enterprise 4 | 5 | A trial version of Faktory Enterprise for macOS is available with each [release](/contribsys/faktory/releases/). 6 | Click to purchase [Faktory Enterprise](https://billing.contribsys.com/fent/). 7 | 8 | ## 1.9.2 9 | 10 | - Run `modernize` on codebase 11 | - Optimize struct alignment with `betteralign` 12 | - Remove reserved tag validation [#490] 13 | 14 | ## 1.9.1 15 | 16 | - Fix "concurrent write" crash with heavily contended per-worker throttles [#483] 17 | - Add support for `DD_DOGSTATSD_URL`, you do not need statsd.toml at all anymore [#479] 18 | `DD_DOGSTATSD_URL=udp://localhost:8125 faktory ...` 19 | - Upgrade datadog client to v5.5.0 [#479] 20 | - Print error if Faktory fails to start [#479] 21 | 22 | ## 1.9.0 23 | 24 | - Add Redis round trip time (in µs) to Statsd, "ops.redis.rtt_us" [#475] 25 | 26 | ## 1.8.0 27 | 28 | - Migrate usage of SHA1 to SHA256 to appease linters 29 | - Fix broken default Statsd namespacing [#433] 30 | 31 | ## 1.7.0 32 | 33 | - Upgrade Redis driver to v9 34 | - Upgrade Datadog driver to v5 35 | 36 | ## 1.6.2 37 | 38 | - Fix crash when batch callback jobs retry [#408] 39 | 40 | ## 1.6.1 41 | 42 | - Support `reserve_for` in cron jobs [#381] 43 | 44 | ## 1.6.0 45 | 46 | - Add support for unlimited license without external network access for 47 | high security environments. See the Licensing wiki page for details. 48 | 49 | ## 1.5.5 50 | 51 | - Cron jobs now support empty arguments and can configure expiry via `expires_in` [#384] 52 | ```toml 53 | [[cron]] 54 | schedule = "*/5 * * * *" 55 | [cron.job] 56 | type = "FiveJob" 57 | args = [] 58 | [cron.job.custom] 59 | expires_in = 60 60 | ``` 61 | 62 | ## 1.5.4 63 | 64 | - Upgrade to Go 1.17 65 | 66 | ## 1.5.1 67 | 68 | - License check now supports HTTP(S)_PROXY env variables. 69 | - Fix crash upon license check if license server is down. 70 | - Releases now provide macOS trial binaries for Apple Silicon. 71 | 72 | ## 1.5.0 73 | 74 | - Implement BYOR - **Bring Your Own Redis**. If Faktory Enterprise sees a 75 | `REDIS_URL` or `REDIS_PROVIDER` variable, it will use that to connect 76 | to Redis rather than starting its own Redis instance. This allows 77 | Faktory Enterprise to be used directly with AWS Elasticache, Heroku 78 | Redis and other SaaS providers. The Web UI /debug page will show you 79 | the current latency to Redis and warn if the latency is above 1ms. 80 | 81 | ## 1.4.2 82 | 83 | - *No significant changes* 84 | 85 | ## 1.4.1 86 | 87 | - Fix for `redis: transaction failed` error during batch processing under heavy load [#305] 88 | 89 | ## 1.4.0 90 | 91 | - Major new feature: **[Job Tracking](https://github.com/contribsys/faktory/wiki/Ent-Tracking)** [#278] 92 | - `-e staging` environment support, limited to 100 connections 93 | 94 | ## 1.3.0 95 | 96 | - Allow custom WebUI tweaks for OEM whitelabeling by customers looking 97 | to integrate Faktory into their own product. [#270] 98 | - Fix crash when pushing batch jobs [#274] 99 | 100 | ## 1.2.0 101 | 102 | - Major features are [Batches](https://github.com/contribsys/faktory/wiki/Ent-Batches) and [Queue Throttling](https://github.com/contribsys/faktory/wiki/Ent-Throttling). 103 | - Initial release. 104 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:e4b30804a381d7603b8a344009987c1ba351c26043501b23b8c7ce21f0b67474" 6 | name = "github.com/BurntSushi/toml" 7 | packages = ["."] 8 | pruneopts = "" 9 | revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005" 10 | version = "v0.3.1" 11 | 12 | [[projects]] 13 | branch = "master" 14 | digest = "1:d743f5ff04492330917b701b421bb60c8a865362ad51b1af867787b9afda5d26" 15 | name = "github.com/apex/log" 16 | packages = ["."] 17 | pruneopts = "" 18 | revision = "d6c5facec1f2ae23a97782ab0ee18af58734346f" 19 | 20 | [[projects]] 21 | digest = "1:0deddd908b6b4b768cfc272c16ee61e7088a60f7fe2f06c547bd3d8e1f8b8e77" 22 | name = "github.com/davecgh/go-spew" 23 | packages = ["spew"] 24 | pruneopts = "" 25 | revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" 26 | version = "v1.1.1" 27 | 28 | [[projects]] 29 | digest = "1:c2db84082861ca42d0b00580d28f4b31aceec477a00a38e1a057fb3da75c8adc" 30 | name = "github.com/go-redis/redis" 31 | packages = [ 32 | ".", 33 | "internal", 34 | "internal/consistenthash", 35 | "internal/hashtag", 36 | "internal/pool", 37 | "internal/proto", 38 | "internal/util", 39 | ] 40 | pruneopts = "" 41 | revision = "75795aa4236dc7341eefac3bbe945e68c99ef9df" 42 | version = "v6.15.3" 43 | 44 | [[projects]] 45 | branch = "master" 46 | digest = "1:b3328b1cb82bf4fc91abdfe1c1c5976e8412ddbd074fc2b81b167b1c12fd2cf3" 47 | name = "github.com/justinas/nosurf" 48 | packages = ["."] 49 | pruneopts = "" 50 | revision = "05988550ea1890c49b702363e53b3afa7aad2b4b" 51 | 52 | [[projects]] 53 | digest = "1:1d7e1867c49a6dd9856598ef7c3123604ea3daabf5b83f303ff457bcbc410b1d" 54 | name = "github.com/pkg/errors" 55 | packages = ["."] 56 | pruneopts = "" 57 | revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" 58 | version = "v0.8.1" 59 | 60 | [[projects]] 61 | digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" 62 | name = "github.com/pmezard/go-difflib" 63 | packages = ["difflib"] 64 | pruneopts = "" 65 | revision = "792786c7400a136282c1664665ae0a8db921c6c2" 66 | version = "v1.0.0" 67 | 68 | [[projects]] 69 | digest = "1:381bcbeb112a51493d9d998bbba207a529c73dbb49b3fd789e48c63fac1f192c" 70 | name = "github.com/stretchr/testify" 71 | packages = ["assert"] 72 | pruneopts = "" 73 | revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053" 74 | version = "v1.3.0" 75 | 76 | [solve-meta] 77 | analyzer-name = "dep" 78 | analyzer-version = 1 79 | input-imports = [ 80 | "github.com/BurntSushi/toml", 81 | "github.com/apex/log", 82 | "github.com/go-redis/redis", 83 | "github.com/justinas/nosurf", 84 | "github.com/stretchr/testify/assert", 85 | ] 86 | solver-name = "gps-cdcl" 87 | solver-version = 1 88 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | [[constraint]] 24 | name = "github.com/BurntSushi/toml" 25 | version = "v0.3.1" 26 | 27 | [[constraint]] 28 | name = "github.com/go-redis/redis" 29 | version = "v6.15.3" 30 | 31 | [[constraint]] 32 | name = "github.com/apex/log" 33 | branch = "master" 34 | 35 | [[constraint]] 36 | name = "github.com/stretchr/testify" 37 | version = "1.3.0" 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Faktory [![CI](https://github.com/contribsys/faktory/actions/workflows/ci.yml/badge.svg)](https://github.com/contribsys/faktory/actions/workflows/ci.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/contribsys/faktory)](https://goreportcard.com/report/github.com/contribsys/faktory) 2 | 3 | At a high level, Faktory is a work server. It is the repository for 4 | background jobs within your application. Jobs have a type and a set of 5 | arguments and are placed into queues for workers to fetch and execute. 6 | 7 | You can use this server to distribute jobs to one or hundreds of 8 | machines. Jobs can be executed with any language by clients using 9 | the Faktory API to fetch a job from a queue. 10 | 11 | ![webui](https://raw.githubusercontent.com/contribsys/faktory/master/docs/webui.png) 12 | 13 | ## Basic Features 14 | 15 | - Jobs are represented as JSON hashes. 16 | - Jobs are pushed to and fetched from queues. 17 | - Jobs are reserved with a timeout, 30 min by default. 18 | - Jobs `FAIL`'d or not `ACK`'d within the reservation timeout are requeued. 19 | - FAIL'd jobs trigger a retry workflow with exponential backoff. 20 | - Contains a comprehensive Web UI for management and monitoring. 21 | 22 | ## Installation 23 | 24 | See the [Installation wiki page](https://github.com/contribsys/faktory/wiki/Installation) for current installation methods. 25 | Here's more info on installation with [Docker](https://github.com/contribsys/faktory/wiki/Docker) and [AWS ECS](https://github.com/contribsys/faktory/wiki/AWS-ECS). 26 | 27 | ## Documentation 28 | 29 | Please [see the Faktory wiki](https://github.com/contribsys/faktory/wiki) for full documentation. 30 | 31 | ## Support 32 | 33 | Open up a Discussion or Issue. 34 | 35 | ## Author 36 | 37 | Mike Perham, @getajobmike, mike @ contribsys.com 38 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please contact support@contribsys.com with details. 6 | -------------------------------------------------------------------------------- /cli/cli_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestReadConfig(t *testing.T) { 12 | wd, _ := os.Getwd() 13 | config, _ := readConfig(filepath.Join(wd, "test-fixtures", "case-one"), "") 14 | 15 | schedule := config["cron"].([]map[string]any) 16 | jobOne := schedule[0]["job"].(map[string]any) 17 | 18 | if len(schedule) < 2 { 19 | t.Fatalf("Schedule did not include both items %v", schedule) 20 | } 21 | 22 | jobTwo := schedule[1]["job"].(map[string]any) 23 | 24 | got := jobOne["type"] 25 | if got != "OneJob" { 26 | t.Errorf("First job in schedule was %v, want OneJob", got) 27 | } 28 | 29 | got = jobTwo["type"] 30 | if got != "TwoJob" { 31 | t.Errorf("Second job in schedule was %v, want TwoJob", got) 32 | } 33 | } 34 | 35 | func TestEnv(t *testing.T) { 36 | err := os.Setenv("FAKTORY_ENV", "staging") 37 | assert.NoError(t, err) 38 | 39 | clix := ParseArguments() 40 | assert.Equal(t, "staging", clix.Environment) 41 | 42 | err = os.Unsetenv("FAKTORY_ENV") 43 | assert.NoError(t, err) 44 | } 45 | -------------------------------------------------------------------------------- /cli/security_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func pwdCfg(value string) map[string]any { 11 | return map[string]any{ 12 | "faktory": map[string]any{ 13 | "password": value, 14 | }, 15 | } 16 | } 17 | 18 | func TestPasswords(t *testing.T) { 19 | emptyCfg := map[string]any{} 20 | // nolint:gosec 21 | pwd := "cce29d6565ab7376" 22 | 23 | t.Run("DevWithPassword", func(t *testing.T) { 24 | cfg := pwdCfg(pwd) 25 | pwd, err := fetchPassword(cfg, "development") 26 | assert.NoError(t, err) 27 | assert.Equal(t, 16, len(pwd)) 28 | assert.Equal(t, "cce29d6565ab7376", pwd) 29 | assert.Equal(t, "********", cfg["faktory"].(map[string]any)["password"]) 30 | }) 31 | 32 | t.Run("DevWithoutPassword", func(t *testing.T) { 33 | pwd, err := fetchPassword(emptyCfg, "development") 34 | assert.NoError(t, err) 35 | assert.Equal(t, "", pwd) 36 | }) 37 | 38 | t.Run("ProductionWithoutPassword", func(t *testing.T) { 39 | pwd, err := fetchPassword(emptyCfg, "production") 40 | assert.Error(t, err) 41 | assert.Equal(t, "", pwd) 42 | }) 43 | 44 | t.Run("ProductionWithFile", func(t *testing.T) { 45 | // nolint:gosec 46 | err := os.WriteFile("/tmp/test-password", []byte("foobar"), os.FileMode(0o666)) 47 | assert.NoError(t, err) 48 | cfg := pwdCfg("/tmp/test-password") 49 | pwd, err := fetchPassword(cfg, "production") 50 | assert.NoError(t, err) 51 | assert.Equal(t, "foobar", pwd) 52 | }) 53 | 54 | t.Run("ProductionWithPassword", func(t *testing.T) { 55 | cfg := pwdCfg(pwd) 56 | pwd, err := fetchPassword(cfg, "production") 57 | assert.NoError(t, err) 58 | assert.Equal(t, 16, len(pwd)) 59 | assert.Equal(t, "cce29d6565ab7376", pwd) 60 | assert.Equal(t, "********", cfg["faktory"].(map[string]any)["password"]) 61 | }) 62 | 63 | t.Run("ProductionEnvPassword", func(t *testing.T) { 64 | os.Setenv("FAKTORY_PASSWORD", "abc123") 65 | 66 | pwd, err := fetchPassword(emptyCfg, "production") 67 | assert.NoError(t, err) 68 | assert.Equal(t, "abc123", pwd) 69 | }) 70 | 71 | os.Unsetenv("FAKTORY_PASSWORD") 72 | 73 | t.Run("ProductionSkipPassword", func(t *testing.T) { 74 | os.Setenv("FAKTORY_SKIP_PASSWORD", "yes") 75 | 76 | pwd, err := fetchPassword(emptyCfg, "production") 77 | assert.NoError(t, err) 78 | assert.Equal(t, "", pwd) 79 | }) 80 | 81 | os.Unsetenv("FAKTORY_SKIP_PASSWORD") 82 | } 83 | -------------------------------------------------------------------------------- /cli/test-fixtures/case-one/conf.d/a.toml: -------------------------------------------------------------------------------- 1 | [[cron]] 2 | schedule = "*/5 * * * *" 3 | [cron.job] 4 | type = "OneJob" 5 | queue = "critical" # EOF newline intentionally omitted -------------------------------------------------------------------------------- /cli/test-fixtures/case-one/conf.d/b.toml: -------------------------------------------------------------------------------- 1 | [[cron]] 2 | schedule = "*/5 * * * *" 3 | [cron.job] 4 | type = "TwoJob" 5 | queue = "critical" 6 | -------------------------------------------------------------------------------- /client/client_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || netbsd || openbsd 2 | 3 | package client 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/contribsys/faktory/util" 13 | ) 14 | 15 | func RssKb() int64 { 16 | // TODO Submit a PR? 17 | cmd := exec.Command("ps", "-p", fmt.Sprintf("%d", os.Getpid()), "-o", "rss=") // nolint:gosec 18 | out, err := cmd.CombinedOutput() 19 | if err != nil { 20 | util.Warnf("Error gathering RSS/BSD: %v", err) 21 | return 0 22 | } 23 | val, err := strconv.Atoi(strings.TrimSpace(string(out))) 24 | if err != nil { 25 | util.Warnf("Error gathering RSS/BSD: %v", err) 26 | return 0 27 | } 28 | return int64(val) 29 | } 30 | -------------------------------------------------------------------------------- /client/client_linux.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | func RssKb() int64 { 12 | path := "/proc/self/status" 13 | if _, err := os.Stat(path); err != nil { 14 | return 0 15 | } 16 | 17 | content, err := ioutil.ReadFile(path) 18 | if err != nil { 19 | return 0 20 | } 21 | 22 | lines := bytes.Split(content, []byte("\n")) 23 | for idx := range lines { 24 | if lines[idx][0] == 'V' { 25 | ls := string(lines[idx]) 26 | if strings.Contains(ls, "VmRSS") { 27 | str := strings.Split(ls, ":")[1] 28 | intt, err := strconv.Atoi(str) 29 | if err != nil { 30 | return 0 31 | } 32 | return int64(intt) 33 | } 34 | } 35 | } 36 | return 0 37 | } 38 | -------------------------------------------------------------------------------- /client/client_windows.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | func RssKb() int64 { 4 | // TODO Submit a PR? 5 | return 0 6 | } 7 | -------------------------------------------------------------------------------- /client/faktory.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | var ( 4 | Name = "Faktory" 5 | Version = "1.9.2" 6 | ) 7 | 8 | // Structs for parsing the INFO response 9 | type FaktoryState struct { 10 | Now string `json:"now"` 11 | ServerUtcTime string `json:"server_utc_time"` 12 | Data DataSnapshot `json:"faktory"` 13 | Server ServerSnapshot `json:"server"` 14 | } 15 | 16 | type DataSnapshot struct { 17 | Queues map[string]uint64 `json:"queues"` 18 | Sets map[string]uint64 `json:"sets"` 19 | Tasks map[string]map[string]any `json:"tasks"` // deprecated 20 | TotalFailures uint64 `json:"total_failures"` 21 | TotalProcessed uint64 `json:"total_processed"` 22 | TotalEnqueued uint64 `json:"total_enqueued"` 23 | TotalQueues uint64 `json:"total_queues"` 24 | } 25 | 26 | type ServerSnapshot struct { 27 | Description string `json:"description"` 28 | Version string `json:"faktory_version"` 29 | Uptime uint64 `json:"uptime"` 30 | Connections uint64 `json:"connections"` 31 | CommandCount uint64 `json:"command_count"` 32 | UsedMemoryMB uint64 `json:"used_memory_mb"` 33 | } 34 | -------------------------------------------------------------------------------- /client/job_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestJob(t *testing.T) { 12 | job := NewJob("yo", 1) 13 | assert.EqualValues(t, 25, *job.Retry) 14 | data, err := json.Marshal(job) 15 | assert.NoError(t, err) 16 | assert.Contains(t, string(data), "retry") 17 | } 18 | 19 | func TestJobCustomOptions(t *testing.T) { 20 | job := NewJob("yo", 1) 21 | expiresAt := time.Now().Add(1 * time.Hour) 22 | job.SetUniqueFor(100). 23 | SetUniqueness(UntilStart). 24 | SetExpiresAt(expiresAt) 25 | 26 | assert.EqualValues(t, 100, job.Custom["unique_for"]) 27 | assert.EqualValues(t, UntilStart, job.Custom["unique_until"]) 28 | assert.EqualValues(t, expiresAt.Format(time.RFC3339Nano), job.Custom["expires_at"]) 29 | 30 | val, ok := job.GetCustom("unique_for") 31 | assert.EqualValues(t, 100, val) 32 | assert.True(t, ok) 33 | } 34 | -------------------------------------------------------------------------------- /client/package.go: -------------------------------------------------------------------------------- 1 | // Code within this package is licensed according to the MPL found 2 | // in the LICENSE file. 3 | 4 | package client 5 | -------------------------------------------------------------------------------- /client/pool.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/contribsys/faktory/internal/pool" 7 | ) 8 | 9 | type Pool struct { 10 | pool.Pool 11 | } 12 | 13 | // NewPool creates a new Pool object through which multiple clients will be managed on your behalf. 14 | // 15 | // Call Get() to retrieve a client instance and Put() to return it to the pool. If you do not call 16 | // Put(), the connection will be leaked, and the pool will stop working once it hits capacity. 17 | // 18 | // Do NOT call Close() on the client, as the lifecycle is managed internally. 19 | // 20 | // The dialer clients in this pool use is determined by the URI scheme in FAKTORY_PROVIDER. 21 | func NewPool(capacity int) (*Pool, error) { 22 | return newPool(capacity, func() (pool.Closeable, error) { return Open() }) 23 | } 24 | 25 | // NewPoolWithDialer creates a new Pool object similar to NewPool but clients will use the 26 | // provided dialer instead of default ones. 27 | func NewPoolWithDialer(capacity int, dialer Dialer) (*Pool, error) { 28 | fn := func() (pool.Closeable, error) { return OpenWithDialer(dialer) } 29 | return newPool(capacity, fn) 30 | } 31 | 32 | type ClientConstructor func() (*Client, error) 33 | 34 | // NewPoolWithClientConstructor creates a new Pool object through which multiple clients will be managed on your behalf. 35 | // This is similar to NewPool, but the clients are created using the provided ClientConstructor function. 36 | // 37 | // Example: 38 | // 39 | // s := DefaultServer() 40 | // NewPoolWithClientConstructor(10, s.Open) 41 | // // OR 42 | // var customDialer Dialer = SomeCustomDialer() 43 | // NewPoolWithClientConstructor(10, func() (*Client, error) { 44 | // return s.OpenWithDialer(customDialer) 45 | // }) 46 | // 47 | // Call Get() to retrieve a client instance and Put() to return it to the pool. If you do not call 48 | // Put(), the connection will be leaked, and the pool will stop working once it hits capacity. 49 | // 50 | // Do NOT call Close() on the client, as the lifecycle is managed internally. 51 | func NewPoolWithClientConstructor(capacity int, fn ClientConstructor) (*Pool, error) { 52 | return newPool(capacity, func() (pool.Closeable, error) { return fn() }) 53 | } 54 | 55 | // newPool creates a *Pool channel with the provided capacity and opener. 56 | func newPool(capacity int, opener pool.Factory) (*Pool, error) { 57 | var p Pool 58 | var err error 59 | p.Pool, err = pool.NewChannelPool(0, capacity, opener) 60 | return &p, err 61 | } 62 | 63 | // Get retrieves a Client from the pool. This Client is created, internally, by calling 64 | // the Open() function, and has all the same behaviors. 65 | func (p *Pool) Get() (*Client, error) { 66 | conn, err := p.Pool.Get() 67 | if err != nil { 68 | return nil, err 69 | } 70 | pc := conn.(*pool.PoolConn) 71 | client, ok := pc.Closeable.(*Client) 72 | if !ok { 73 | // Because we control the entire lifecycle of the pool, internally, this should never happen. 74 | panic(fmt.Sprintf("Connection is not a Faktory client instance: %+v", conn)) 75 | } 76 | client.poolConn = pc 77 | return client, nil 78 | } 79 | 80 | // Put returns a client to the pool. 81 | func (p *Pool) Put(client *Client) { 82 | client.poolConn.Close() 83 | } 84 | 85 | func (p *Pool) With(fn func(conn *Client) error) error { 86 | conn, err := p.Get() 87 | if err != nil { 88 | return err 89 | } 90 | defer p.Put(conn) 91 | return fn(conn) 92 | } 93 | -------------------------------------------------------------------------------- /client/tracking.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/contribsys/faktory/util" 9 | ) 10 | 11 | type JobTrack struct { 12 | Jid string `json:"jid"` 13 | Description string `json:"desc,omitempty"` 14 | State string `json:"state"` 15 | UpdatedAt string `json:"updated_at"` 16 | Percent int `json:"percent,omitempty"` 17 | } 18 | 19 | func (c *Client) TrackGet(jid string) (*JobTrack, error) { 20 | err := c.writeLine(c.wtr, "TRACK GET", []byte(jid)) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | data, err := c.readResponse(c.rdr) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | var trck JobTrack 31 | err = util.JsonUnmarshal(data, &trck) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return &trck, nil 37 | } 38 | 39 | type setJobTrack struct { 40 | Jid string `json:"jid"` 41 | Description string `json:"desc,omitempty"` 42 | ReserveUntil string `json:"reserve_until,omitempty"` 43 | Percent int `json:"percent,omitempty"` 44 | } 45 | 46 | func (c *Client) TrackSet(jid string, percent int, desc string, reserveUntil *time.Time) error { 47 | if jid == "" { 48 | return fmt.Errorf("Job Track missing JID") 49 | } 50 | 51 | tset := setJobTrack{ 52 | Jid: jid, 53 | Description: desc, 54 | Percent: percent, 55 | } 56 | if reserveUntil != nil && time.Now().Before(*reserveUntil) { 57 | tset.ReserveUntil = util.Thens(*reserveUntil) 58 | } 59 | return c.trackSet(&tset) 60 | } 61 | 62 | func (c *Client) trackSet(tset *setJobTrack) error { 63 | data, err := json.Marshal(tset) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | err = c.writeLine(c.wtr, "TRACK SET", data) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | return c.ok(c.rdr) 74 | } 75 | -------------------------------------------------------------------------------- /cmd/faktory-cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/contribsys/faktory/client" 11 | ) 12 | 13 | func main() { 14 | cl, err := client.Open() 15 | if err != nil { 16 | fail(err) 17 | } 18 | defer cl.Close() 19 | 20 | data, err := cl.Info() 21 | if err != nil { 22 | fail(err) 23 | } 24 | svr := data["server"].(map[string]any) 25 | fmt.Printf("Connected to %s %s\n", svr["description"], svr["faktory_version"]) 26 | 27 | reader := bufio.NewReader(os.Stdin) 28 | 29 | for { 30 | fmt.Printf("> ") 31 | line, err := reader.ReadString('\n') 32 | if err != nil { 33 | if err == io.EOF { 34 | fmt.Println("") 35 | return 36 | } 37 | fail(err) 38 | } 39 | 40 | if strings.HasPrefix(line, "END") { 41 | break 42 | } 43 | 44 | resp, err := cl.Generic(line) 45 | if err != nil { 46 | fmt.Printf("%s\n", err.Error()) 47 | } 48 | fmt.Printf("%s\n", resp) 49 | } 50 | } 51 | 52 | func fail(err error) { 53 | fmt.Println(err.Error()) 54 | os.Exit(-1) 55 | } 56 | -------------------------------------------------------------------------------- /cmd/faktory/daemon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/contribsys/faktory/cli" 8 | "github.com/contribsys/faktory/client" 9 | "github.com/contribsys/faktory/util" 10 | "github.com/contribsys/faktory/webui" 11 | ) 12 | 13 | func logPreamble() { 14 | log.SetFlags(0) 15 | log.Println(client.Name, client.Version) 16 | log.Printf("Copyright © %d Contributed Systems LLC", time.Now().Year()) 17 | log.Println("Licensed under the GNU Affero Public License 3.0") 18 | } 19 | 20 | func main() { 21 | logPreamble() 22 | 23 | opts := cli.ParseArguments() 24 | util.InitLogger(opts.LogLevel) 25 | util.Debugf("Options: %v", opts) 26 | 27 | s, stopper, err := cli.BuildServer(&opts) 28 | if err != nil { 29 | util.Error("Unable to create Faktory server", err) 30 | return 31 | } 32 | defer func() { _ = stopper() }() 33 | 34 | err = s.Boot() 35 | if err != nil { 36 | util.Error("Unable to boot the command server", err) 37 | return 38 | } 39 | 40 | s.Register(webui.Subsystem(opts.WebBinding)) 41 | 42 | go cli.HandleSignals(s) 43 | go func() { 44 | err = s.Run() 45 | if err != nil { 46 | util.Error("Unable to start Faktory", err) 47 | } 48 | }() 49 | 50 | <-s.Stopper() 51 | s.Stop(nil) 52 | } 53 | -------------------------------------------------------------------------------- /docs/commands-validity.rst: -------------------------------------------------------------------------------- 1 | Commands validity 2 | ----------------- 3 | 4 | +---------+---------------------+------------------------------------------------------------------------+ 5 | | | Role | State | 6 | +=========+==========+==========+==============+================+============+=======+=============+=====+ 7 | | Command | Producer | Consumer | DISCONNECTED | NOT_IDENTIFIED | IDENTIFIED | QUIET | TERMINATING | END | 8 | +---------+----------+----------+--------------+----------------+------------+-------+-------------+-----+ 9 | | HELLO | ✓ | ✓ | | ✓ | | | | | 10 | +---------+----------+----------+--------------+----------------+------------+-------+-------------+-----+ 11 | | FLUSH | ✓ | ✓ | | | ✓ | | | | 12 | +---------+----------+----------+--------------+----------------+------------+-------+-------------+-----+ 13 | | INFO | ✓ | ✓ | | | ✓ | | | | 14 | +---------+----------+----------+--------------+----------------+------------+-------+-------------+-----+ 15 | | END | ✓ | ✓ | | | ✓ | ✓ | ✓ | | 16 | +---------+----------+----------+--------------+----------------+------------+-------+-------------+-----+ 17 | | PUSH | ✓ | | | | ✓ | | | | 18 | +---------+----------+----------+--------------+----------------+------------+-------+-------------+-----+ 19 | | FETCH | | ✓ | | | ✓ | | | | 20 | +---------+----------+----------+--------------+----------------+------------+-------+-------------+-----+ 21 | | ACK | | ✓ | | | ✓ | ✓ | ✓ | | 22 | +---------+----------+----------+--------------+----------------+------------+-------+-------------+-----+ 23 | | FAIL | | ✓ | | | ✓ | ✓ | ✓ | | 24 | +---------+----------+----------+--------------+----------------+------------+-------+-------------+-----+ 25 | | BEAT | | ✓ | | | ✓ | ✓ | | | 26 | +---------+----------+----------+--------------+----------------+------------+-------+-------------+-----+ 27 | -------------------------------------------------------------------------------- /docs/webui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contribsys/faktory/bfef21eabd53fc8073fcf327decfcbc5cf8947f3/docs/webui.png -------------------------------------------------------------------------------- /example/config.toml: -------------------------------------------------------------------------------- 1 | [queues] 2 | # disable backpressure by default 3 | backpressure = 0 4 | 5 | [queues.default] 6 | # the default queue will allow up to 100,000 jobs. After that, 7 | # further PUSHes will get an error until the queue is drained 8 | # below that threshold. 9 | backpressure = 100000 10 | 11 | [security] 12 | 13 | [security.tls] 14 | public_key = "/etc/faktory/tls/public.crt" 15 | private_key = "/etc/faktory/tls/private.crt" 16 | -------------------------------------------------------------------------------- /example/cron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contribsys/faktory/bfef21eabd53fc8073fcf327decfcbc5cf8947f3/example/cron.png -------------------------------------------------------------------------------- /example/cronloop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contribsys/faktory/bfef21eabd53fc8073fcf327decfcbc5cf8947f3/example/cronloop.png -------------------------------------------------------------------------------- /example/webui-throttle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contribsys/faktory/bfef21eabd53fc8073fcf327decfcbc5cf8947f3/example/webui-throttle.png -------------------------------------------------------------------------------- /example/whitelabel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contribsys/faktory/bfef21eabd53fc8073fcf327decfcbc5cf8947f3/example/whitelabel.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/contribsys/faktory 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.5.0 7 | github.com/contribsys/faktory_worker_go v1.7.0 8 | github.com/justinas/nosurf v1.2.0 9 | github.com/redis/go-redis/v9 v9.7.3 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | ) 18 | 19 | require ( 20 | github.com/stretchr/testify v1.10.0 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 2 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 4 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 5 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 6 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 7 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 8 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/contribsys/faktory_worker_go v1.7.0 h1:YSZRj/nSjn+2ms9ooHIOHAahHJehXutgD46b+0lHDP4= 10 | github.com/contribsys/faktory_worker_go v1.7.0/go.mod h1:JRw4PvanwLgX5IIQazw6W5zg/Rg7/Fg6YrkdH4gJJPc= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 15 | github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= 16 | github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= 17 | github.com/justinas/nosurf v1.2.0 h1:yMs1bSRrNiwXk4AS6n8vL2Ssgpb9CB25T/4xrixaK0s= 18 | github.com/justinas/nosurf v1.2.0/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 22 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 23 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 24 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /internal/pool/pool_test.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type Thing struct { 10 | } 11 | 12 | func (t Thing) Close() error { 13 | return nil 14 | } 15 | 16 | func TestPool(t *testing.T) { 17 | pool, err := NewChannelPool(0, 10, func() (Closeable, error) { return Thing{}, nil }) 18 | assert.NoError(t, err) 19 | assert.NotNil(t, pool) 20 | assert.Equal(t, 0, pool.Len()) 21 | 22 | thng, err := pool.Get() 23 | assert.NoError(t, err) 24 | assert.NotNil(t, thng) 25 | assert.Equal(t, 0, pool.Len()) 26 | 27 | thng.Close() 28 | assert.Equal(t, 1, pool.Len()) 29 | 30 | pool.Close() 31 | assert.Equal(t, 0, pool.Len()) 32 | } 33 | -------------------------------------------------------------------------------- /manager/middleware.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/contribsys/faktory/client" 7 | ) 8 | 9 | type MiddlewareFunc func(ctx context.Context, next func() error) error 10 | type MiddlewareChain []MiddlewareFunc 11 | 12 | type helperKey string 13 | 14 | const ( 15 | MiddlewareHelperKey helperKey = "_mh" 16 | ) 17 | 18 | type Context interface { 19 | Job() *client.Job 20 | Reservation() *Reservation 21 | Manager() Manager 22 | } 23 | 24 | type Ctx struct { 25 | job *client.Job 26 | mgr *manager 27 | res *Reservation 28 | } 29 | 30 | func (c Ctx) Reservation() *Reservation { 31 | return c.res 32 | } 33 | 34 | func (c Ctx) Job() *client.Job { 35 | return c.job 36 | } 37 | 38 | func (c Ctx) Manager() Manager { 39 | return c.mgr 40 | } 41 | 42 | // Returning a Halt error in a middleware will stop the middleware execution 43 | // chain. The server will return the Halt to the client. You can use "ERR" 44 | // for the code to signal an unexpected error or use a well-defined code for 45 | // an error case which the client might be interested in, e.g. "NOTUNIQUE". 46 | func Halt(code string, msg string) error { 47 | return ExpectedError(code, msg) 48 | } 49 | 50 | // Middleware can use this to restart the fetch process. Useful if the job 51 | // fetched from Redis is invalid and should be discarded rather than returned 52 | // to the worker. 53 | func Discard(msg string) error { 54 | return ExpectedError("DISCARD", msg) 55 | } 56 | 57 | // Run the given job through the given middleware chain. 58 | // `final` is the function called if the entire chain passes the job along. 59 | func callMiddleware(ctx context.Context, chain MiddlewareChain, final func() error) error { 60 | if len(chain) == 0 { 61 | return final() 62 | } 63 | 64 | link := chain[0] 65 | rest := chain[1:] 66 | return link(ctx, func() error { return callMiddleware(ctx, rest, final) }) 67 | } 68 | -------------------------------------------------------------------------------- /manager/scheduler.go: -------------------------------------------------------------------------------- 1 | package manager 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/contribsys/faktory/client" 9 | "github.com/contribsys/faktory/storage" 10 | "github.com/contribsys/faktory/util" 11 | ) 12 | 13 | func (m *manager) Purge(ctx context.Context, when time.Time) (int64, error) { 14 | // TODO We need to purge the dead set if it collects more 15 | // than N elements. The dead set shouldn't be able to collect 16 | // millions or billions of jobs. Sidekiq uses a default max size 17 | // of 10,000 jobs. 18 | dead, err := m.store.Dead().RemoveBefore(ctx, util.Thens(when), 100, func([]byte) error { 19 | return nil 20 | }) 21 | if err != nil { 22 | return 0, err 23 | } 24 | return dead, nil 25 | } 26 | 27 | func (m *manager) EnqueueScheduledJobs(ctx context.Context, when time.Time) (int64, error) { 28 | return m.schedule(ctx, when, m.store.Scheduled()) 29 | } 30 | 31 | func (m *manager) RetryJobs(ctx context.Context, when time.Time) (int64, error) { 32 | return m.schedule(ctx, when, m.store.Retries()) 33 | } 34 | 35 | func (m *manager) schedule(ctx context.Context, when time.Time, set storage.SortedSet) (int64, error) { 36 | total := int64(0) 37 | for { 38 | count, err := set.RemoveBefore(ctx, util.Thens(when), 100, func(data []byte) error { 39 | var job client.Job 40 | if err := util.JsonUnmarshal(data, &job); err != nil { 41 | return fmt.Errorf("cannot unmarshal job payload: %w", err) 42 | } 43 | 44 | if err := m.enqueue(ctx, &job); err != nil { 45 | return fmt.Errorf("cannot push job to %q queue: %w", job.Queue, err) 46 | } 47 | return nil 48 | }) 49 | total += count 50 | if err != nil { 51 | return total, err 52 | } 53 | if count != 100 { 54 | break 55 | } 56 | } 57 | return total, nil 58 | } 59 | -------------------------------------------------------------------------------- /packaging/root/usr/share/faktory/init/faktory.conf: -------------------------------------------------------------------------------- 1 | description "faktory: background jobs" 2 | 3 | start on runlevel [2345] 4 | stop on runlevel [016] 5 | 6 | setgid adm 7 | umask 0002 8 | 9 | # if we crash, restart 10 | respawn 11 | # don't try to restart anymore if we fail 5 times in 5 seconds 12 | respawn limit 5 5 13 | 14 | exec /usr/bin/faktory -e production -b localhost:7419 15 | -------------------------------------------------------------------------------- /packaging/root/usr/share/faktory/init/faktory.rpm.conf: -------------------------------------------------------------------------------- 1 | description "Faktory background jobs" 2 | 3 | start on runlevel [2345] 4 | stop on runlevel [016] 5 | 6 | umask 0002 7 | 8 | # if we crash, restart 9 | respawn 10 | # don't try to restart anymore if we fail 5 times in 5 seconds 11 | respawn limit 5 5 12 | 13 | exec /usr/bin/faktory -e production -b localhost:7419 14 | -------------------------------------------------------------------------------- /packaging/root/usr/share/faktory/systemd/faktory.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=faktory 3 | # start us only once the network and logging subsystems are available 4 | After=syslog.target network.target 5 | 6 | # See these pages for lots of options: 7 | # http://0pointer.de/public/systemd-man/systemd.service.html 8 | # http://0pointer.de/public/systemd-man/systemd.exec.html 9 | [Service] 10 | Type=simple 11 | 12 | # required for Redis under low-memory conditions 13 | # alternatively you can add 'vm.overcommit_memory = 1' 14 | # to /etc/sysctl.conf and reboot 15 | ExecStartPre=/sbin/sysctl vm.overcommit_memory=1 16 | 17 | # In the production environment, a password is required and created for you 18 | # in /etc/faktory/password. 19 | # -b controls the command port binding 20 | # -w controls the Web UI binding 21 | # 22 | # If you remove `-e production` and `/etc/faktory/password`, Faktory will 23 | # not require a password but also be utterly without security: achtung! 24 | ExecStart=/usr/bin/faktory -e production -b 0.0.0.0:7419 -w 0.0.0.0:7420 25 | 26 | # Change any config? 27 | # Run `systemctl reload faktory` 28 | ExecReload=/bin/kill -HUP $MAINPID 29 | Group=adm 30 | UMask=0002 31 | 32 | # if we crash, restart 33 | RestartSec=1 34 | Restart=on-failure 35 | 36 | # use syslog for logging 37 | StandardOutput=syslog 38 | StandardError=syslog 39 | SyslogIdentifier=faktory 40 | 41 | [Install] 42 | WantedBy=multi-user.target 43 | -------------------------------------------------------------------------------- /packaging/scripts/postinst.deb.systemd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ ! -d /etc/faktory ]; then 6 | mkdir /etc/faktory 7 | head -5 /dev/urandom | md5sum | cut -c1-16 > /etc/faktory/password 8 | chmod 600 /etc/faktory/password 9 | fi 10 | 11 | if which systemctl > /dev/null; then 12 | echo "Using systemd to control faktory" 13 | cp /usr/share/faktory/systemd/faktory.service /lib/systemd/system/faktory.service 14 | 15 | systemctl daemon-reload || : 16 | systemctl enable faktory || : 17 | systemctl start faktory || : 18 | else 19 | echo "Couldn't find systemd to control faktory, cannot proceed." 20 | echo "Open an issue and tell us about your system." 21 | exit 1 22 | fi 23 | 24 | cat <<"TXT" 25 | 26 | __ _ _ 27 | / _| __ _| | _| |_ ___ _ __ _ _ 28 | | |_ / _` | |/ / __/ _ \| '__| | | | 29 | | _| (_| | <| || (_) | | | |_| | 30 | |_| \__,_|_|\_\\__\___/|_| \__, | 31 | |___/ 32 | 33 | 34 | Welcome to Faktory, let's get working! 35 | 36 | TXT 37 | exit 0 38 | -------------------------------------------------------------------------------- /packaging/scripts/postinst.deb.upstart: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ ! -d /etc/faktory ]; then 6 | mkdir /etc/faktory 7 | head -5 /dev/urandom | md5sum | cut -c1-16 > /etc/faktory/password 8 | chmod 600 /etc/faktory/password 9 | fi 10 | 11 | if [ -d /etc/init ]; then 12 | echo "Using upstart to control faktory" 13 | cp -r /usr/share/faktory/init/faktory.conf /etc/init/faktory.conf 14 | initctl start faktory || : 15 | else 16 | echo "Couldn't find upstart to control faktory, cannot proceed." 17 | echo "Open an issue and tell us about your system." 18 | exit 1 19 | fi 20 | 21 | cat <<"TXT" 22 | 23 | __ _ _ 24 | / _| __ _| | _| |_ ___ _ __ _ _ 25 | | |_ / _` | |/ / __/ _ \| '__| | | | 26 | | _| (_| | <| || (_) | | | |_| | 27 | |_| \__,_|_|\_\\__\___/|_| \__, | 28 | |___/ 29 | 30 | 31 | Thanks for installing faktory! 32 | 33 | TXT 34 | exit 0 35 | -------------------------------------------------------------------------------- /packaging/scripts/postinst.rpm.systemd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ ! -d /etc/faktory ]; then 6 | mkdir /etc/faktory 7 | head -5 /dev/urandom | md5sum | cut -c1-16 > /etc/faktory/password 8 | chmod 600 /etc/faktory/password 9 | fi 10 | 11 | if which systemctl > /dev/null; then 12 | echo "Using systemd to control faktory" 13 | cp /usr/share/faktory/systemd/faktory.service /lib/systemd/system/faktory.service 14 | 15 | systemctl daemon-reload || : 16 | if [ "$1" = 1 ] ; then 17 | # first time install 18 | systemctl enable faktory || : 19 | systemctl start faktory || : 20 | else 21 | echo "Upgrading faktory" 22 | fi 23 | else 24 | echo "Couldn't find systemd to control faktory, cannot proceed." 25 | echo "Open an issue and tell us about your system." 26 | exit 1 27 | fi 28 | 29 | cat <<"TXT" 30 | __ _ _ 31 | / _| __ _| | _| |_ ___ _ __ _ _ 32 | | |_ / _` | |/ / __/ _ \| '__| | | | 33 | | _| (_| | <| || (_) | | | |_| | 34 | |_| \__,_|_|\_\\__\___/|_| \__, | 35 | |___/ 36 | 37 | 38 | Welcome to Faktory, let's get working! 39 | 40 | TXT 41 | exit 0 42 | -------------------------------------------------------------------------------- /packaging/scripts/postinst.rpm.upstart: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ ! -d /etc/faktory ]; then 6 | mkdir /etc/faktory 7 | head -5 /dev/urandom | md5sum | cut -c1-16 > /etc/faktory/password 8 | chmod 600 /etc/faktory/password 9 | fi 10 | 11 | # CentOS 6.x doesn't have an adm group. 12 | # CentOS 7.x and Ubuntu do. Standardize! 13 | if ! groupadd adm ; then 14 | true 15 | fi 16 | 17 | if [ -d /etc/init ]; then 18 | echo "Using upstart to control faktory" 19 | cp -r /usr/share/faktory/init/faktory.rpm.conf /etc/init/faktory.conf 20 | if [ "$1" = 1 ] ; then 21 | # first time install, upgrades are restarted in postrm 22 | initctl start faktory || : 23 | fi 24 | else 25 | echo "Couldn't find upstart to control faktory, cannot proceed." 26 | echo "Open an issue and tell us about your system." 27 | exit 1 28 | fi 29 | 30 | cat <<"TXT" 31 | 32 | __ _ _ 33 | / _| __ _| | _| |_ ___ _ __ _ _ 34 | | |_ / _` | |/ / __/ _ \| '__| | | | 35 | | _| (_| | <| || (_) | | | |_| | 36 | |_| \__,_|_|\_\\__\___/|_| \__, | 37 | |___/ 38 | 39 | 40 | Thanks for installing faktory! 41 | 42 | TXT 43 | 44 | exit 0 45 | -------------------------------------------------------------------------------- /packaging/scripts/postrm.deb.systemd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | systemctl daemon-reload || : 6 | 7 | if [ "$1" = "remove" ] ; then 8 | rm -f /lib/systemd/system/faktory.service 9 | fi 10 | 11 | if [ "$1" = "purge" ] ; then 12 | rm -rf /etc/faktory 13 | fi 14 | -------------------------------------------------------------------------------- /packaging/scripts/postrm.deb.upstart: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html 6 | 7 | rm -f /etc/init/faktory.conf 8 | 9 | if [ "$1" = "purge" ] ; then 10 | echo "Purging faktory configuration" 11 | rm -rf /etc/faktory 12 | fi 13 | -------------------------------------------------------------------------------- /packaging/scripts/postrm.rpm.systemd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # https://docs.fedoraproject.org/en-US/Fedora_Draft_Documentation/0.1/html/RPM_Guide/ch09s04s05.html 6 | 7 | systemctl daemon-reload || : 8 | if [ "$1" -ge 1 ] ; then 9 | # Package upgrade, not uninstall 10 | systemctl try-restart faktory || : 11 | fi 12 | -------------------------------------------------------------------------------- /packaging/scripts/postrm.rpm.upstart: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # https://docs.fedoraproject.org/en-US/Fedora_Draft_Documentation/0.1/html/RPM_Guide/ch09s04s05.html 6 | 7 | if [ "$1" -ge 1 ]; then 8 | # Package upgrade, not uninstall 9 | echo Restarting faktory 10 | initctl restart faktory || : 11 | else 12 | rm -f /etc/init/faktory.conf 13 | fi 14 | -------------------------------------------------------------------------------- /packaging/scripts/prerm.deb.systemd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ "$1" = "remove" ] ; then 5 | echo Stopping and disabling systemd service 6 | # Package removal, not upgrade 7 | systemctl --no-reload disable faktory || : 8 | fi 9 | systemctl stop faktory || : 10 | -------------------------------------------------------------------------------- /packaging/scripts/prerm.deb.upstart: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo Stopping faktory 5 | initctl stop faktory || : 6 | -------------------------------------------------------------------------------- /packaging/scripts/prerm.rpm.systemd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ "$1" -eq 0 ] ; then 5 | echo Stopping and disabling systemd service 6 | # Package removal, not upgrade 7 | systemctl --no-reload disable faktory || : 8 | systemctl stop faktory || : 9 | fi 10 | -------------------------------------------------------------------------------- /packaging/scripts/prerm.rpm.upstart: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ "$1" = 0 ] ; then 5 | echo Stopping faktory 6 | initctl stop faktory || : 7 | fi 8 | -------------------------------------------------------------------------------- /server/commands_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/contribsys/faktory/client" 10 | "github.com/contribsys/faktory/util" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestCommands(t *testing.T) { 15 | // util.LogInfo = true 16 | // util.LogDebug = true 17 | runServer("localhost:4478", func(s *Server) { 18 | t.Run("queue pause", func(t *testing.T) { 19 | c := dummyConnection() 20 | assert.NotNil(t, c.Context) 21 | 22 | queue(c, s, "QUEUE PAUSE *") 23 | txt := output(c) 24 | assert.Equal(t, "+OK\r\n", txt) 25 | 26 | queue(c, s, "QUEUE UNPAUSE *") 27 | txt = output(c) 28 | assert.Equal(t, "-ERR no such QUEUE subcommand: UNPAUSE\r\n", txt) 29 | 30 | queue(c, s, "QUEUE RESUME *") 31 | txt = output(c) 32 | assert.Equal(t, "+OK\r\n", txt) 33 | 34 | queue(c, s, "QUEUE REMOVE foo") 35 | txt = output(c) 36 | assert.Equal(t, "+OK\r\n", txt) 37 | 38 | queue(c, s, "QUEUE REMOVE *") 39 | txt = output(c) 40 | assert.Equal(t, "+OK\r\n", txt) 41 | 42 | queue(c, s, "QUEUE PAUSE default") 43 | txt = output(c) 44 | assert.Equal(t, "+OK\r\n", txt) 45 | 46 | queue(c, s, "QUEUE RESUME default") 47 | txt = output(c) 48 | assert.Equal(t, "+OK\r\n", txt) 49 | }) 50 | 51 | t.Run("queue latency", func(t *testing.T) { 52 | c := dummyConnection() 53 | assert.NotNil(t, c.Context) 54 | 55 | queue(c, s, "QUEUE LATENCY *") 56 | txt := output(c) 57 | assert.Equal(t, "-ERR QUEUE LATENCY does not support wildcards\r\n", txt) 58 | 59 | queue(c, s, "QUEUE LATENCY default") 60 | txt = output(c) 61 | assert.Equal(t, "$13\r\n{\"default\":0}\r\n", txt) 62 | 63 | ctx := c.Context 64 | job := client.NewJob("jobtype", 1, 2, "mike") 65 | assert.NoError(t, s.Manager().Push(ctx, job)) 66 | 67 | queue(c, s, "queue latency default foo") 68 | txt = output(c) 69 | assert.Regexp(t, regexp.MustCompile("\"default\":0.\\d{4}"), txt) 70 | assert.Regexp(t, regexp.MustCompile("\"foo\":0"), txt) 71 | }) 72 | 73 | t.Run("PUSHB", func(t *testing.T) { 74 | jobs := []*client.Job{} 75 | for range 10 { 76 | job := client.NewJob("Mike", 1, 2, "foo") 77 | jobs = append(jobs, job) 78 | } 79 | 80 | c := dummyConnection() 81 | flush(c, s, "flush") 82 | txt := output(c) 83 | assert.Equal(t, "+OK\r\n", txt) 84 | 85 | data, err := json.Marshal(jobs) 86 | assert.NoError(t, err) 87 | cmd := fmt.Sprintf("pushb %s", data) 88 | pushBulk(c, s, cmd) 89 | txt = output(c) 90 | // no errors, all 10 pushed 91 | assert.Equal(t, "$2\r\n{}\r\n", txt) 92 | x, _ := s.CurrentState() 93 | data, _ = json.Marshal(x) 94 | util.Infof("State: %s", string(data)) 95 | qsize := x.Data.Queues["default"] 96 | assert.EqualValues(t, 10, qsize) 97 | 98 | job1 := jobs[0] 99 | job1.Type = "" 100 | data, err = json.Marshal(jobs) 101 | assert.NoError(t, err) 102 | cmd = fmt.Sprintf("pushb %s", data) 103 | pushBulk(c, s, cmd) 104 | txt = output(c) 105 | assert.Equal(t, fmt.Sprintf("$57\r\n{%q:\"jobs must have a jobtype parameter\"}\r\n", job1.Jid), txt) 106 | }) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/contribsys/faktory/util" 4 | 5 | // This is the ultimate scalability limitation in Faktory, 6 | // we only allow this many connections to Redis. 7 | var DefaultMaxPoolSize uint64 = 2000 8 | 9 | type ServerOptions struct { 10 | GlobalConfig map[string]any 11 | Binding string 12 | StorageDirectory string 13 | RedisSock string 14 | ConfigDirectory string 15 | Environment string 16 | Password string 17 | PoolSize uint64 18 | } 19 | 20 | func (so *ServerOptions) String(subsys string, key string, defval string) string { 21 | val := so.Config(subsys, key, defval) 22 | str, ok := val.(string) 23 | if !ok { 24 | util.Warnf("Config error: %s/%s is not a String", subsys, key) 25 | return defval 26 | } 27 | return str 28 | } 29 | 30 | func (so *ServerOptions) Config(subsys string, key string, defval any) any { 31 | mapp, ok := so.GlobalConfig[subsys] 32 | if !ok { 33 | return defval 34 | } 35 | 36 | maps, ok := mapp.(map[string]any) 37 | if !ok { 38 | util.Warnf("Invalid configuration, expected a %s subsystem, using default", subsys) 39 | return defval 40 | } 41 | 42 | val, ok := maps[key] 43 | if !ok { 44 | return defval 45 | } 46 | return val 47 | } 48 | -------------------------------------------------------------------------------- /server/connection.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "strconv" 9 | 10 | "github.com/contribsys/faktory/manager" 11 | ) 12 | 13 | // Represents a connection to a faktory client. 14 | // 15 | // faktory reuses the same wire protocol as Redis: RESP. 16 | // It's a nice trade-off between human-readable and efficient. 17 | // Shout out to antirez for his nice design document on it. 18 | // https://redis.io/topics/protocol 19 | type Connection struct { 20 | client *ClientData 21 | conn io.WriteCloser 22 | buf *bufio.Reader 23 | context.Context 24 | } 25 | 26 | func (c *Connection) Close() error { 27 | return c.conn.Close() 28 | } 29 | 30 | func (c *Connection) Error(cmd string, err error) error { 31 | if re, ok := err.(manager.KnownError); ok { 32 | _, err = fmt.Fprintf(c.conn, "-%s\r\n", re.Error()) 33 | } else { 34 | _, err = fmt.Fprintf(c.conn, "-ERR %s\r\n", err.Error()) 35 | } 36 | return err 37 | } 38 | 39 | func (c *Connection) Ok() error { 40 | _, err := c.conn.Write([]byte("+OK\r\n")) 41 | return err 42 | } 43 | 44 | func (c *Connection) Number(val int) error { 45 | _, err := c.conn.Write([]byte(":" + strconv.Itoa(val) + "\r\n")) 46 | return err 47 | } 48 | 49 | func (c *Connection) Result(msg []byte) error { 50 | if msg == nil { 51 | _, err := c.conn.Write([]byte("$-1\r\n")) 52 | return err 53 | } 54 | 55 | _, err := c.conn.Write([]byte("$" + strconv.Itoa(len(msg)) + "\r\n")) 56 | if err != nil { 57 | return err 58 | } 59 | _, err = c.conn.Write(msg) 60 | if err != nil { 61 | return err 62 | } 63 | _, err = c.conn.Write([]byte("\r\n")) 64 | return err 65 | } 66 | -------------------------------------------------------------------------------- /server/connection_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestConnectionBasics(t *testing.T) { 16 | dc := dummyConnection() 17 | 18 | assert.NotNil(t, dc) 19 | 20 | err := dc.Ok() 21 | assert.NoError(t, err) 22 | assert.Equal(t, "+OK\r\n", output(dc)) 23 | 24 | err = dc.Number(123) 25 | assert.NoError(t, err) 26 | assert.Equal(t, ":123\r\n", output(dc)) 27 | 28 | err = dc.Result(nil) 29 | assert.NoError(t, err) 30 | assert.Equal(t, "$-1\r\n", output(dc)) 31 | 32 | err = dc.Result([]byte("{some:jobjson}")) 33 | assert.NoError(t, err) 34 | assert.Equal(t, "$14\r\n{some:jobjson}\r\n", output(dc)) 35 | 36 | err = dc.Error("bad command", fmt.Errorf("permission denied")) 37 | assert.NoError(t, err) 38 | assert.Equal(t, "-ERR permission denied\r\n", output(dc)) 39 | 40 | dc.Close() 41 | assert.Equal(t, "", output(dc)) 42 | } 43 | 44 | type TestingWriteCloser struct { 45 | *bufio.Writer 46 | output *bytes.Buffer 47 | } 48 | 49 | func (wc *TestingWriteCloser) Close() error { 50 | return wc.Flush() 51 | } 52 | 53 | func (wc *TestingWriteCloser) Output() string { 54 | wc.Flush() 55 | data := wc.output.String() 56 | wc.output.Reset() 57 | return data 58 | } 59 | 60 | func output(dc *Connection) string { 61 | tc := dc.conn.(*TestingWriteCloser) 62 | return tc.Output() 63 | } 64 | 65 | func dummyConnection() *Connection { 66 | writeBuffer := bytes.NewBuffer(make([]byte, 0)) 67 | wc := &TestingWriteCloser{output: writeBuffer, Writer: bufio.NewWriter(writeBuffer)} 68 | 69 | return &Connection{ 70 | client: dummyClientData(), 71 | conn: wc, 72 | buf: bufio.NewReader(strings.NewReader("")), 73 | Context: context.Background(), 74 | } 75 | } 76 | 77 | func dummyClientData() *ClientData { 78 | return &ClientData{ 79 | Hostname: "foobar.example.com", 80 | Wid: "123k1h23kh", 81 | Pid: 70086, 82 | Labels: []string{"golang", "someapp"}, 83 | StartedAt: time.Now(), 84 | lastHeartbeat: time.Now(), 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /server/scanner.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/contribsys/faktory/storage" 9 | "github.com/contribsys/faktory/util" 10 | ) 11 | 12 | type scannerTask func(context.Context, time.Time) (int64, error) 13 | 14 | type scanner struct { 15 | set storage.SortedSet 16 | task scannerTask 17 | name string 18 | jobs int64 19 | cycles int64 20 | walltime int64 21 | } 22 | 23 | func (s *scanner) Name() string { 24 | return s.name 25 | } 26 | 27 | func (s *scanner) Execute(ctx context.Context) error { 28 | start := time.Now() 29 | count, err := s.task(ctx, start) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | if count > 0 { 35 | util.Infof("%s processed %d jobs", s.name, count) 36 | } 37 | 38 | end := time.Now() 39 | atomic.AddInt64(&s.cycles, 1) 40 | atomic.AddInt64(&s.jobs, count) 41 | atomic.AddInt64(&s.walltime, end.Sub(start).Nanoseconds()) 42 | return nil 43 | } 44 | 45 | func (s *scanner) Stats(ctx context.Context) map[string]any { 46 | return map[string]any{ 47 | "enqueued": atomic.LoadInt64(&s.jobs), 48 | "cycles": atomic.LoadInt64(&s.cycles), 49 | "size": s.set.Size(ctx), 50 | "wall_time_sec": (float64(atomic.LoadInt64(&s.walltime)) / 1000000000), 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server/subsystem.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | type Subsystem interface { 4 | Name() string 5 | 6 | // Called when the server is configured but before it starts accepting client connections. 7 | Start(*Server) error 8 | 9 | // Called every time Faktory reloads the global config for the Server. 10 | // Each subsystem is responsible for diffing its own config and making 11 | // necessary changes. 12 | Reload(*Server) error 13 | 14 | // Shutdown is signaled by the Server.Stopper() channel. Subsystems should 15 | // select on it to be notified when the server is shutting down. 16 | } 17 | 18 | // register a global handler to be called when the Server instance 19 | // has finished booting but before it starts listening. 20 | func (s *Server) Register(x Subsystem) { 21 | s.Subsystems = append(s.Subsystems, x) 22 | } 23 | -------------------------------------------------------------------------------- /server/task_runner.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/contribsys/faktory/util" 11 | ) 12 | 13 | /* 14 | * The task runner allows us to run internal tasks on 15 | * a recurring schedule, e.g. "reap old heartbeats every 30 seconds". 16 | * 17 | * tr = newTaskRunner() 18 | * tr.AddTask("heartbeat reaper", reapHeartbeats, 30) 19 | * ts.Run(...) 20 | */ 21 | type taskRunner struct { 22 | tasks []*task 23 | 24 | walltimeNs int64 25 | cycles int64 26 | executions int64 27 | mutex sync.RWMutex 28 | } 29 | 30 | type task struct { 31 | runner Taskable 32 | every int64 33 | runs int64 34 | walltimeNs int64 35 | } 36 | 37 | type Taskable interface { 38 | Name() string 39 | Execute(context.Context) error 40 | Stats(context.Context) map[string]any 41 | } 42 | 43 | func newTaskRunner() *taskRunner { 44 | return &taskRunner{ 45 | tasks: make([]*task, 0), 46 | } 47 | } 48 | 49 | func (ts *taskRunner) AddTask(sec int64, thing Taskable) { 50 | var tsk task 51 | tsk.runner = thing 52 | tsk.every = sec 53 | ts.mutex.Lock() 54 | ts.tasks = append(ts.tasks, &tsk) 55 | ts.mutex.Unlock() 56 | } 57 | 58 | func (ts *taskRunner) Run(stopper chan bool) { 59 | go func() { 60 | // add random jitter so the runner goroutine doesn't fire at 000ms 61 | time.Sleep(time.Duration(rand.Float64()) * time.Second) //nolint:gosec 62 | timer := time.NewTicker(1 * time.Second) 63 | defer timer.Stop() 64 | 65 | for { 66 | ts.cycle() 67 | select { 68 | case <-timer.C: 69 | case <-stopper: 70 | util.Debug("Stopping scheduled tasks") 71 | return 72 | } 73 | } 74 | }() 75 | } 76 | 77 | func (ts *taskRunner) Stats() map[string]map[string]any { 78 | data := map[string]map[string]any{} 79 | 80 | ctx := context.Background() 81 | ts.mutex.RLock() 82 | defer ts.mutex.RUnlock() 83 | for _, task := range ts.tasks { 84 | data[task.runner.Name()] = task.runner.Stats(ctx) 85 | } 86 | return data 87 | } 88 | 89 | func (ts *taskRunner) cycle() { 90 | count := int64(0) 91 | start := time.Now() 92 | sec := start.Unix() 93 | ts.mutex.RLock() 94 | defer ts.mutex.RUnlock() 95 | for _, t := range ts.tasks { 96 | t := t 97 | if sec%t.every != 0 { 98 | continue 99 | } 100 | tstart := time.Now() 101 | // util.Debugf("Running task %s", t.runner.Name()) 102 | err := t.runner.Execute(context.Background()) 103 | tend := time.Now() 104 | if err != nil { 105 | util.Warnf("Error running task %s: %v", t.runner.Name(), err) 106 | } 107 | atomic.AddInt64(&t.runs, 1) 108 | atomic.AddInt64(&t.walltimeNs, tend.Sub(tstart).Nanoseconds()) 109 | count++ 110 | } 111 | end := time.Now() 112 | atomic.AddInt64(&ts.cycles, 1) 113 | atomic.AddInt64(&ts.executions, count) 114 | atomic.AddInt64(&ts.walltimeNs, end.Sub(start).Nanoseconds()) 115 | } 116 | 117 | func (s *Server) startTasks() { 118 | ts := newTaskRunner() 119 | // scan the various sets, looking for things to do 120 | ts.AddTask(5, &scanner{name: "Scheduled", set: s.store.Scheduled(), task: s.manager.EnqueueScheduledJobs}) 121 | ts.AddTask(5, &scanner{name: "Retries", set: s.store.Retries(), task: s.manager.RetryJobs}) 122 | ts.AddTask(60, &scanner{name: "Dead", set: s.store.Dead(), task: s.manager.Purge}) 123 | 124 | // reaps job reservations which have expired 125 | ts.AddTask(15, &reservationReaper{s.manager, 0}) 126 | // reaps workers who have not heartbeated 127 | ts.AddTask(15, &beatReaper{s.workers, 0}) 128 | 129 | ts.Run(s.Stopper()) 130 | s.taskRunner = ts 131 | } 132 | -------------------------------------------------------------------------------- /server/tasks.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | "time" 7 | 8 | "github.com/contribsys/faktory/manager" 9 | ) 10 | 11 | type reservationReaper struct { 12 | m manager.Manager 13 | count int64 14 | } 15 | 16 | func (r *reservationReaper) Name() string { 17 | return "Busy" 18 | } 19 | 20 | func (r *reservationReaper) Execute(ctx context.Context) error { 21 | count, err := r.m.ReapExpiredJobs(ctx, time.Now()) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | atomic.AddInt64(&r.count, int64(count)) 27 | return nil 28 | } 29 | 30 | func (r *reservationReaper) Stats(ctx context.Context) map[string]any { 31 | return map[string]any{ 32 | "size": r.m.WorkingCount(), 33 | "reaped": atomic.LoadInt64(&r.count), 34 | } 35 | } 36 | 37 | /* 38 | * Removes any heartbeat records over 1 minute old. 39 | */ 40 | type beatReaper struct { 41 | w *workers 42 | count int64 43 | } 44 | 45 | func (r *beatReaper) Name() string { 46 | return "Workers" 47 | } 48 | 49 | func (r *beatReaper) Execute(ctx context.Context) error { 50 | count := r.w.reapHeartbeats(time.Now().Add(-1 * time.Minute)) 51 | atomic.AddInt64(&r.count, int64(count)) 52 | return nil 53 | } 54 | 55 | func (r *beatReaper) Stats(context.Context) map[string]any { 56 | return map[string]any{ 57 | "size": r.w.Count(), 58 | "reaped": atomic.LoadInt64(&r.count), 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/workers_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestClientData(t *testing.T) { 12 | t.Parallel() 13 | 14 | cw, err := clientDataFromHello("") 15 | assert.Error(t, err) 16 | assert.Nil(t, cw) 17 | 18 | cw, err = clientDataFromHello("{") 19 | assert.Error(t, err) 20 | assert.Nil(t, cw) 21 | 22 | cw, err = clientDataFromHello("{}") 23 | assert.NoError(t, err) 24 | assert.NotNil(t, cw) 25 | assert.False(t, cw.IsConsumer()) 26 | 27 | ahoy := `{"hostname":"MikeBookPro.local","wid":"78629a0f5f3f164f","pid":40275,"labels":["blue","seven"],"salt":"123456","pwdhash":"958d51602bbfbd18b2a084ba848a827c29952bfef170c936419b0922994c0589"}` 28 | cw, err = clientDataFromHello(ahoy) 29 | assert.NoError(t, err) 30 | assert.NotNil(t, cw) 31 | assert.True(t, cw.IsConsumer()) 32 | 33 | assert.Equal(t, Running, cw.state) 34 | assert.False(t, cw.IsQuiet()) 35 | 36 | cw.Signal(Quiet) 37 | assert.Equal(t, Quiet, cw.state) 38 | assert.True(t, cw.IsQuiet()) 39 | 40 | cw.Signal(Terminate) 41 | assert.Equal(t, Terminate, cw.state) 42 | assert.True(t, cw.IsQuiet()) 43 | 44 | // can't go back to quiet 45 | cw.Signal(Quiet) 46 | assert.Equal(t, Terminate, cw.state) 47 | assert.True(t, cw.IsQuiet()) 48 | } 49 | 50 | func TestWorkers(t *testing.T) { 51 | t.Parallel() 52 | 53 | workers := newWorkers() 54 | assert.Equal(t, 0, workers.Count()) 55 | 56 | beat := &ClientBeat{ 57 | Wid: "78629a0f5f3f164f", 58 | } 59 | entry, ok := workers.heartbeat(beat) 60 | assert.Equal(t, 0, workers.Count()) 61 | assert.Nil(t, entry) 62 | assert.False(t, ok) 63 | 64 | client := &ClientData{ 65 | Hostname: "MikeBookPro.local", 66 | Wid: "78629a0f5f3f164f", 67 | connections: map[io.Closer]bool{}, 68 | } 69 | entry, ok = workers.setupHeartbeat(client, &cls{}) 70 | assert.NotNil(t, entry) 71 | assert.False(t, ok) 72 | 73 | entry, ok = workers.heartbeat(beat) 74 | assert.Equal(t, 1, workers.Count()) 75 | assert.NotNil(t, entry) 76 | assert.True(t, ok) 77 | 78 | before := time.Now() 79 | entry, ok = workers.heartbeat(beat) 80 | after := time.Now() 81 | assert.Equal(t, 1, workers.Count()) 82 | assert.NotNil(t, entry) 83 | assert.True(t, ok) 84 | assert.LessOrEqual(t, before, entry.lastHeartbeat) 85 | assert.LessOrEqual(t, entry.lastHeartbeat, after) 86 | 87 | assert.Equal(t, Running, entry.state) 88 | beat.CurrentState = "quiet" 89 | entry, _ = workers.heartbeat(beat) 90 | assert.Equal(t, Quiet, entry.state) 91 | assert.True(t, entry.IsQuiet()) 92 | 93 | beat.CurrentState = "" 94 | entry, _ = workers.heartbeat(beat) 95 | assert.Equal(t, Quiet, entry.state) 96 | 97 | beat.CurrentState = "terminate" 98 | entry, _ = workers.heartbeat(beat) 99 | assert.Equal(t, Terminate, entry.state) 100 | 101 | count := workers.reapHeartbeats(client.lastHeartbeat) 102 | assert.Equal(t, 1, workers.Count()) 103 | assert.Equal(t, 0, count) 104 | 105 | count = workers.reapHeartbeats(time.Now()) 106 | assert.Equal(t, 0, workers.Count()) 107 | assert.Equal(t, 1, count) 108 | } 109 | 110 | type cls struct{} 111 | 112 | func (c cls) Close() error { 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /storage/history.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | func (store *redisStore) Success(ctx context.Context) error { 13 | daystr := time.Now().Format("2006-01-02") 14 | store.rclient.Incr(ctx, fmt.Sprintf("processed:%s", daystr)) 15 | store.rclient.Incr(ctx, "processed") 16 | return nil 17 | } 18 | 19 | func (store *redisStore) TotalProcessed(ctx context.Context) uint64 { 20 | return uint64(store.rclient.IncrBy(ctx, "processed", 0).Val()) // nolint:gosec 21 | } 22 | func (store *redisStore) TotalFailures(ctx context.Context) uint64 { 23 | return uint64(store.rclient.IncrBy(ctx, "failures", 0).Val()) // nolint:gosec 24 | } 25 | 26 | func (store *redisStore) Failure(ctx context.Context) error { 27 | store.rclient.Incr(ctx, "processed") 28 | store.rclient.Incr(ctx, "failures") 29 | 30 | daystr := time.Now().Format("2006-01-02") 31 | store.rclient.Incr(ctx, fmt.Sprintf("processed:%s", daystr)) 32 | store.rclient.Incr(ctx, fmt.Sprintf("failures:%s", daystr)) 33 | return nil 34 | } 35 | 36 | func (store *redisStore) History(ctx context.Context, days int, fn func(day string, procCnt uint64, failCnt uint64)) error { 37 | if days > 180 { 38 | return errors.New("days value can't be greater than 180") 39 | } 40 | ts := time.Now() 41 | daystrs := make([]string, days) 42 | fails := make([]*redis.IntCmd, days) 43 | procds := make([]*redis.IntCmd, days) 44 | 45 | _, err := store.rclient.Pipelined(ctx, func(pipe redis.Pipeliner) error { 46 | for idx := range days { 47 | daystr := ts.Format("2006-01-02") 48 | daystrs[idx] = daystr 49 | procds[idx] = pipe.IncrBy(ctx, fmt.Sprintf("processed:%s", daystr), 0) 50 | fails[idx] = pipe.IncrBy(ctx, fmt.Sprintf("failures:%s", daystr), 0) 51 | ts = ts.Add(-24 * time.Hour) 52 | } 53 | return nil 54 | }) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | for idx := range days { 60 | fn(daystrs[idx], uint64(procds[idx].Val()), uint64(fails[idx].Val())) // nolint:gosec 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /storage/history_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestStats(t *testing.T) { 12 | withRedis(t, "history", func(t *testing.T, store Store) { 13 | bg := context.Background() 14 | store.Flush(bg) 15 | var err error 16 | 17 | for i := range 10000 { 18 | if i%100 == 99 { 19 | err = store.Failure(bg) 20 | assert.NoError(t, err) 21 | } else { 22 | err = store.Success(bg) 23 | assert.NoError(t, err) 24 | } 25 | } 26 | 27 | assert.EqualValues(t, 10000, store.TotalProcessed(bg)) 28 | assert.EqualValues(t, 100, store.TotalFailures(bg)) 29 | 30 | err = store.Failure(bg) 31 | assert.NoError(t, err) 32 | err = store.Success(bg) 33 | assert.NoError(t, err) 34 | 35 | assert.EqualValues(t, 10002, store.TotalProcessed(bg)) 36 | assert.EqualValues(t, 101, store.TotalFailures(bg)) 37 | 38 | hash := map[string][2]uint64{} 39 | err = store.History(bg, 3, func(day string, p, f uint64) { 40 | hash[day] = [2]uint64{p, f} 41 | }) 42 | assert.NoError(t, err) 43 | assert.Equal(t, 3, len(hash)) 44 | 45 | daystr := time.Now().Format("2006-01-02") 46 | counts := hash[daystr] 47 | assert.NotNil(t, counts) 48 | assert.EqualValues(t, 10002, counts[0]) 49 | assert.EqualValues(t, 101, counts[1]) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /storage/queue_redis.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/contribsys/faktory/client" 9 | "github.com/contribsys/faktory/util" 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | type redisQueue struct { 14 | store *redisStore 15 | name string 16 | done bool 17 | } 18 | 19 | func (store *redisStore) NewQueue(name string) *redisQueue { 20 | return &redisQueue{ 21 | name: name, 22 | store: store, 23 | done: false, 24 | } 25 | } 26 | 27 | func (q *redisQueue) Pause(ctx context.Context) error { 28 | return q.store.rclient.SAdd(ctx, "paused", q.name).Err() 29 | } 30 | 31 | func (q *redisQueue) Resume(ctx context.Context) error { 32 | return q.store.rclient.SRem(ctx, "paused", q.name).Err() 33 | } 34 | 35 | func (q *redisQueue) IsPaused(ctx context.Context) bool { 36 | b, _ := q.store.rclient.SIsMember(ctx, "paused", q.name).Result() 37 | return b 38 | } 39 | 40 | func (q *redisQueue) Close() { 41 | q.done = true 42 | } 43 | 44 | func (q *redisQueue) Name() string { 45 | return q.name 46 | } 47 | 48 | func (q *redisQueue) Page(ctx context.Context, start int64, count int64, fn func(index int, data []byte) error) error { 49 | index := 0 50 | 51 | slice, err := q.store.rclient.LRange(ctx, q.name, start, start+count).Result() 52 | for idx := range slice { 53 | err = fn(index, []byte(slice[idx])) 54 | if err != nil { 55 | return err 56 | } 57 | index += 1 58 | } 59 | return err 60 | } 61 | 62 | func (q *redisQueue) Each(ctx context.Context, fn func(index int, data []byte) error) error { 63 | return q.Page(ctx, 0, -1, fn) 64 | } 65 | 66 | func (q *redisQueue) Clear(ctx context.Context) (uint64, error) { 67 | q.store.mu.Lock() 68 | defer q.store.mu.Unlock() 69 | 70 | _, err := q.store.rclient.Pipelined(ctx, func(pipe redis.Pipeliner) error { 71 | pipe.Unlink(ctx, q.name) 72 | pipe.SRem(ctx, "queues", q.name) 73 | pipe.SRem(ctx, "paused", q.name) 74 | return nil 75 | }) 76 | if err != nil { 77 | return 0, err 78 | } 79 | 80 | delete(q.store.queueSet, q.name) 81 | return 0, nil 82 | } 83 | 84 | func (q *redisQueue) init(ctx context.Context) error { 85 | util.Debugf("Queue init: %s %d elements", q.name, q.Size(ctx)) 86 | return nil 87 | } 88 | 89 | func (q *redisQueue) Size(ctx context.Context) uint64 { 90 | return uint64(q.store.rclient.LLen(ctx, q.name).Val()) // nolint:gosec 91 | } 92 | 93 | func (q *redisQueue) Add(ctx context.Context, job *client.Job) error { 94 | job.EnqueuedAt = util.Nows() 95 | data, err := json.Marshal(job) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | return q.Push(ctx, data) 101 | } 102 | 103 | func (q *redisQueue) Push(ctx context.Context, payload []byte) error { 104 | return q.store.rclient.LPush(ctx, q.name, payload).Err() 105 | } 106 | 107 | // non-blocking, returns immediately if there's nothing enqueued 108 | func (q *redisQueue) Pop(ctx context.Context) ([]byte, error) { 109 | if q.done { 110 | return nil, nil 111 | } 112 | 113 | return q._pop(ctx) 114 | } 115 | 116 | func (q *redisQueue) _pop(ctx context.Context) ([]byte, error) { 117 | val, err := q.store.rclient.RPop(ctx, q.name).Result() 118 | if val == "" { 119 | return nil, nil 120 | } 121 | return []byte(val), err 122 | } 123 | 124 | func (q *redisQueue) BPop(ctx context.Context) ([]byte, error) { 125 | val, err := q.store.rclient.BRPop(ctx, 2*time.Second, q.name).Result() 126 | if err != nil { 127 | if err == redis.Nil { 128 | return nil, nil 129 | } 130 | return nil, err 131 | } 132 | 133 | return []byte(val[1]), nil 134 | } 135 | 136 | func (q *redisQueue) Delete(ctx context.Context, vals [][]byte) error { 137 | for idx := range vals { 138 | err := q.store.rclient.LRem(ctx, q.name, 1, vals[idx]).Err() 139 | if err != nil { 140 | return err 141 | } 142 | } 143 | 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /storage/raw.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | var ( 11 | ErrNilValue = errors.New("nil value not allowed") 12 | ) 13 | 14 | type KV interface { 15 | Get(ctx context.Context, key string) ([]byte, error) 16 | Set(ctx context.Context, key string, value []byte) error 17 | } 18 | 19 | // Provide a basic KV scratch pad, for misc feature usage. 20 | // Not optimized so "big" features don't use this. 21 | type redisKV struct { 22 | store *redisStore 23 | } 24 | 25 | func (s *redisStore) Raw() KV { 26 | return &redisKV{s} 27 | } 28 | 29 | func (kv *redisKV) Get(ctx context.Context, key string) ([]byte, error) { 30 | value, err := kv.store.rclient.Get(ctx, key).Result() 31 | if err != nil { 32 | if err == redis.Nil { 33 | return nil, nil 34 | } 35 | return nil, err 36 | } 37 | return []byte(value), nil 38 | } 39 | 40 | func (kv *redisKV) Set(ctx context.Context, key string, value []byte) error { 41 | if value == nil { 42 | return ErrNilValue 43 | } 44 | return kv.store.rclient.Set(ctx, key, value, 0).Err() 45 | } 46 | -------------------------------------------------------------------------------- /storage/redis_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestRedisKV(t *testing.T) { 13 | withRedis(t, "default", func(t *testing.T, store Store) { 14 | ctx := context.Background() 15 | store.Flush(ctx) 16 | kv := store.Raw() 17 | assert.NotNil(t, kv) 18 | 19 | val, err := kv.Get(ctx, "mike") 20 | assert.NoError(t, err) 21 | assert.Nil(t, val) 22 | 23 | err = kv.Set(ctx, "bob", nil) 24 | assert.Equal(t, ErrNilValue, err) 25 | 26 | err = kv.Set(ctx, "mike", []byte("bob")) 27 | assert.NoError(t, err) 28 | 29 | val, err = kv.Get(ctx, "mike") 30 | assert.NoError(t, err) 31 | assert.NotNil(t, val) 32 | assert.Equal(t, "bob", string(val)) 33 | }) 34 | } 35 | 36 | func withRedis(t *testing.T, name string, fn func(*testing.T, Store)) { 37 | t.Parallel() 38 | 39 | dir := fmt.Sprintf("/tmp/faktory-test-%s", name) 40 | defer os.RemoveAll(dir) 41 | 42 | sock := fmt.Sprintf("%s/redis.sock", dir) 43 | stopper, err := Boot(dir, sock) 44 | if stopper != nil { 45 | defer func() { _ = stopper() }() 46 | } 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | store, err := Open(sock, 10) 52 | if err != nil { 53 | panic(err) 54 | } 55 | defer store.Close() 56 | 57 | fn(t, store) 58 | } 59 | -------------------------------------------------------------------------------- /storage/types.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/contribsys/faktory/client" 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | type BackupInfo struct { 12 | Id int64 13 | FileCount int32 14 | Size int64 15 | Timestamp int64 16 | } 17 | 18 | type Store interface { 19 | Close() error 20 | Retries() SortedSet 21 | Scheduled() SortedSet 22 | Working() SortedSet 23 | Dead() SortedSet 24 | ExistingQueue(ctx context.Context, name string) (q Queue, ok bool) 25 | GetQueue(ctx context.Context, name string) (Queue, error) 26 | EachQueue(ctx context.Context, eachFn func(Queue)) 27 | Stats(ctx context.Context) map[string]string 28 | EnqueueAll(ctx context.Context, from SortedSet) error 29 | EnqueueFrom(ctx context.Context, from SortedSet, data []byte) error 30 | PausedQueues(ctx context.Context) ([]string, error) 31 | 32 | History(ctx context.Context, days int, fn func(day string, procCnt uint64, failCnt uint64)) error 33 | Success(ctx context.Context) error 34 | Failure(ctx context.Context) error 35 | TotalProcessed(ctx context.Context) uint64 36 | TotalFailures(ctx context.Context) uint64 37 | 38 | // Clear the database of all job data. 39 | // Equivalent to Redis's FLUSHDB 40 | Flush(ctx context.Context) error 41 | 42 | Raw() KV 43 | Redis 44 | } 45 | 46 | type Redis interface { 47 | Redis() *redis.Client 48 | } 49 | 50 | type Queue interface { 51 | Name() string 52 | Size(ctx context.Context) uint64 53 | 54 | Pause(ctx context.Context) error 55 | Resume(ctx context.Context) error 56 | IsPaused(ctx context.Context) bool 57 | 58 | Add(ctx context.Context, job *client.Job) error 59 | Push(ctx context.Context, data []byte) error 60 | 61 | Pop(ctx context.Context) ([]byte, error) 62 | BPop(ctx context.Context) ([]byte, error) 63 | Clear(ctx context.Context) (uint64, error) 64 | 65 | Each(ctx context.Context, fn func(index int, data []byte) error) error 66 | Page(ctx context.Context, start int64, count int64, fn func(index int, data []byte) error) error 67 | 68 | Delete(ctx context.Context, keys [][]byte) error 69 | } 70 | 71 | type SortedEntry interface { 72 | Value() []byte 73 | Key() ([]byte, error) 74 | Job() (*client.Job, error) 75 | } 76 | 77 | type SortedSet interface { 78 | Name() string 79 | Size(ctx context.Context) uint64 80 | Clear(ctx context.Context) error 81 | 82 | Add(ctx context.Context, job *client.Job) error 83 | AddElement(ctx context.Context, timestamp string, jid string, payload []byte) error 84 | 85 | Get(ctx context.Context, key []byte) (SortedEntry, error) 86 | Page(ctx context.Context, start int, count int, fn func(index int, e SortedEntry) error) (int, error) 87 | Each(ctx context.Context, fn func(idx int, e SortedEntry) error) error 88 | 89 | Find(ctx context.Context, match string, fn func(idx int, e SortedEntry) error) error 90 | 91 | // bool is whether or not the element was actually removed from the sset. 92 | // the scheduler and other things can be operating on the sset concurrently 93 | // so we need to be careful about the data changing under us. 94 | Remove(ctx context.Context, key []byte) (bool, error) 95 | RemoveElement(ctx context.Context, timestamp string, jid string) (bool, error) 96 | RemoveBefore(ctx context.Context, timestamp string, maxCount int64, fn func(data []byte) error) (int64, error) 97 | RemoveEntry(ctx context.Context, ent SortedEntry) error 98 | 99 | // Move the given key from this SortedSet to the given 100 | // SortedSet atomically. The given func may mutate the payload and 101 | // return a new tstamp. 102 | MoveTo(ctx context.Context, sset SortedSet, entry SortedEntry, newtime time.Time) error 103 | } 104 | -------------------------------------------------------------------------------- /test/auth/password: -------------------------------------------------------------------------------- 1 | cce29d6565ab7376 2 | -------------------------------------------------------------------------------- /test/cfg/private.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNLCm+Mz3aqpxg 3 | Y1lwiZjKBpWfT6W8ZJCRS75P5RAIAH3pOqO7g3ZmhQprA9KbtuKqjuDrdLnnkEC3 4 | jmE/7bLMFAVVbF+N1dj+1CtMsdH5T8aweC62XuaeHIc2MMvXiSX3QcAMUjdaXWW4 5 | wCNQhRJwbvE1XWcgzuoPJneeJdvCQLbW+c2B0vTI8k+lexz9iuVReWBumaFXubeE 6 | vy4BeITGcWPnTsRZ7KJlPUwkfHxWxStatwI+gjCXv76tOIM+6cWh0QHbUqnMpiOv 7 | 0G4SkHunnI9DGxRv7jZBrQYCWqxO/G/bFceIoWCmTaGna+sMqprdH2SBHYDGIotn 8 | s+sekDrXAgMBAAECggEABQjTuJe9S51AH7zUnnmmJKu8bLC7PIRWaFYKiIW/VGyJ 9 | VRMXr5IZC+71ZdGDINBeB8PG1R4dVFyh7zz9bRxKlwE/0/0uhTNBvdvVmxjWupGo 10 | CHboXGVABDyrgPctBy3Z9IIOQL8lURozdw7ULOAqQJlclMm2FKZHrAay0PrECdeA 11 | oKoxoRldlqrjSxyaRAt13vrVbaVB3cvtnGlIeSlRfSx8m04u0U1uWGlBlb8xhgOR 12 | 65j6+bKNAPaE2qxOuxyrIu5DbKnyjfky3Mf1FApmrpjmu3XBwrZuhnPFNzLeMx0w 13 | YsYUX19Sbqp1YpisK9ok5OQ0AiA+DP8SD70y7OicQQKBgQDqntooG9LRqIcMs6cw 14 | JijOXTQilwnYjRAfpRnkkp8Fu33gsmXa7Oa1j9rVcBnpLxBCTFR2iFmOxpm7f9JX 15 | XnNC+zVgHYgxftX1L9yX+nkuSi/bWSWRhEJDcCOSG1vTV2x1aoYb8RqUyKIpF+h1 16 | SES1bE08cDW19T07Wmdxz6W6kQKBgQDf3luahCbvGQqmR53LEyJHNuQc2FgPdAKv 17 | kiL4bf3F68vQPuMuvsPim1UyyxmG4rhA1BrnUZq2nERZ+Cv7MXLcy51Q1Lq9K/93 18 | WUbOxYsT+3NeQRNEJVpfhZZ0ALUZmfMNp5aemrH//1yBEuk4TLikPbn3AQ114wjo 19 | vwfqgfPC5wKBgAlxp3ph1FSYIgeC28H2Z0IXQlf6GG1dED8V2Dr5I+mJKIH47Fyp 20 | mIfKaJaa0pAuUss4Y1X6GxDCMcH4XTEjHiSeFAHwbmD/qAEckhaUAHi6h76ekgKP 21 | flNUmjnxW/rf0//N7+QECnver5hT3AmMhSeAWoOKSL8wReyzsOJF53fhAoGBANLj 22 | W/XGMgsg0uhrJJlN8AeYDPGjV+lOxszv5GOU8fAFvZzx8P9zE4KgA3Vy4Bwx7ZKc 23 | fK+WLyGBOd5rK7tZDLQ0V4DytOtJzEF453wXmXl8cWTD9stGSMkdResHU5LHdLBT 24 | RE8quS3IODMbRnoTxAhsYYfvBOgdtKHUezeNrbzXAoGAZai4rNxmuPKsZS2uuT45 25 | 6edkS0OW9n8y6NHso8chxBsGdXuz45bbWdndW3Xs+4BTaahUOJ59ILvN+s4ZEG1z 26 | 6GALGp1Jg4dAvMCks6tvH2mPTK69Zjp8NZSuiNpJbhL1mHtmCchhZPEKW8f+BKLx 27 | 3W139Ps4mqU+H6oo45iOS1Y= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/cfg/public.cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEQzCCAyugAwIBAgIUOf/FXcVBElRQYGEmbBqVoepecV4wDQYJKoZIhvcNAQEL 3 | BQAwgbAxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIDAZPcmVnb24xETAPBgNVBAcMCFBv 4 | cnRsYW5kMSAwHgYDVQQKDBdDb250cmlidXRlZCBTeXN0ZW1zIExMQzEQMA4GA1UE 5 | CwwHRmFrdG9yeTEkMCIGA1UEAwwbZmFrdG9yeS10ZXN0LmNvbnRyaWJzeXMuY29t 6 | MSMwIQYJKoZIhvcNAQkBFhRhZG1pbkBjb250cmlic3lzLmNvbTAeFw0yNDAyMTky 7 | MDE2MzNaFw0yNTAyMTgyMDE2MzNaMIGwMQswCQYDVQQGEwJVUzEPMA0GA1UECAwG 8 | T3JlZ29uMREwDwYDVQQHDAhQb3J0bGFuZDEgMB4GA1UECgwXQ29udHJpYnV0ZWQg 9 | U3lzdGVtcyBMTEMxEDAOBgNVBAsMB0Zha3RvcnkxJDAiBgNVBAMMG2Zha3Rvcnkt 10 | dGVzdC5jb250cmlic3lzLmNvbTEjMCEGCSqGSIb3DQEJARYUYWRtaW5AY29udHJp 11 | YnN5cy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDNLCm+Mz3a 12 | qpxgY1lwiZjKBpWfT6W8ZJCRS75P5RAIAH3pOqO7g3ZmhQprA9KbtuKqjuDrdLnn 13 | kEC3jmE/7bLMFAVVbF+N1dj+1CtMsdH5T8aweC62XuaeHIc2MMvXiSX3QcAMUjda 14 | XWW4wCNQhRJwbvE1XWcgzuoPJneeJdvCQLbW+c2B0vTI8k+lexz9iuVReWBumaFX 15 | ubeEvy4BeITGcWPnTsRZ7KJlPUwkfHxWxStatwI+gjCXv76tOIM+6cWh0QHbUqnM 16 | piOv0G4SkHunnI9DGxRv7jZBrQYCWqxO/G/bFceIoWCmTaGna+sMqprdH2SBHYDG 17 | Iotns+sekDrXAgMBAAGjUzBRMB0GA1UdDgQWBBTydABM8WHFVsPGVpf5JXKqXuR3 18 | qjAfBgNVHSMEGDAWgBTydABM8WHFVsPGVpf5JXKqXuR3qjAPBgNVHRMBAf8EBTAD 19 | AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBoofu4cyxCdZgq15+A4Zj1WyHX3DlSK+N0 20 | FUphoukwLLMyeo/WiTPkL76CLcP/0nu2GwbBmlNsGkBSJPhcxwrZXm3tGZ1fchrD 21 | b6T60hYWskXju+D+LjoG8MXzImcF1FboCyVTePK+2+cy5Lmm1IwFaa+TeqyLeQnN 22 | uM8YLDn+bx7/G+uHkZoCQCTb3iXl5gz197aVOKLfMC/8FIAO9lQspUHEOEzKPB5e 23 | dkVQo4uwi9+8doXVVHfJmK1je/6/LNWaNKOeCkXRx5DICMdpsV+/4GPpR+YrTbBc 24 | W4Q23OOsGsBNHVVcOC8fePHwPPfdEZqttwqo9yZUaizqpQCKRQ7M 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /test/load/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "os" 8 | "strconv" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | 13 | faktory "github.com/contribsys/faktory/client" 14 | ) 15 | 16 | var ( 17 | jobs = int64(30000) 18 | threads = int64(10) 19 | opsCount []int = nil 20 | queues = []string{ 21 | "queue0", "queue1", "queue2", "queue3", "queue4", 22 | } 23 | pops = int64(0) 24 | pushes = int64(0) 25 | ) 26 | 27 | func main() { 28 | argc := len(os.Args) 29 | if argc > 1 { 30 | aops, err := strconv.ParseInt(os.Args[1], 10, 64) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | jobs = aops 35 | } 36 | 37 | if argc > 2 { 38 | athreads, err := strconv.ParseInt(os.Args[2], 10, 64) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | threads = athreads 43 | } 44 | 45 | fmt.Printf("Running loadtest with %d jobs and %d threads\n", jobs, threads) 46 | 47 | client, err := faktory.Open() 48 | if err != nil { 49 | handleError(err) 50 | return 51 | } 52 | defer client.Close() 53 | client.Flush() 54 | 55 | opsCount = make([]int, threads) 56 | run() 57 | } 58 | 59 | func run() { 60 | start := time.Now() 61 | var waiter sync.WaitGroup 62 | for i := int64(0); i < threads; i++ { 63 | waiter.Add(1) 64 | go func(idx int64) { 65 | defer waiter.Done() 66 | stress(idx) 67 | }(i) 68 | } 69 | 70 | waiter.Wait() 71 | stop := time.Since(start) 72 | fmt.Printf("Processed %d pushes and %d pops in %2f seconds, rate: %f jobs/s\n", pushes, pops, stop.Seconds(), float64(jobs)/stop.Seconds()) 73 | } 74 | 75 | func stress(idx int64) { 76 | opsCount[idx] = 0 77 | 78 | client, err := faktory.Open() 79 | if err != nil { 80 | handleError(err) 81 | return 82 | } 83 | defer client.Close() 84 | 85 | randomQueues := shuffle(queues) 86 | 87 | for { 88 | if idx%2 == 0 { 89 | push(client, randomQueue()) 90 | newp := atomic.AddInt64(&pushes, 1) 91 | if newp >= jobs { 92 | return 93 | } 94 | } else { 95 | pop(client, randomQueues) 96 | newp := atomic.AddInt64(&pops, 1) 97 | if newp >= jobs { 98 | return 99 | } 100 | } 101 | opsCount[idx]++ 102 | } 103 | } 104 | 105 | func randomQueue() string { 106 | return queues[rand.Intn(len(queues))] //nolint:gosec 107 | } 108 | 109 | func pop(client *faktory.Client, queues []string) { 110 | job, err := client.Fetch(queues...) 111 | if err != nil { 112 | handleError(err) 113 | return 114 | } 115 | if job == nil { 116 | return // timeout? 117 | } 118 | if rand.Intn(100) == 99 { //nolint:gosec 119 | err = client.Fail(job.Jid, os.ErrClosed, nil) 120 | } else { 121 | err = client.Ack(job.Jid) 122 | } 123 | if err != nil { 124 | handleError(err) 125 | return 126 | } 127 | } 128 | 129 | func push(client *faktory.Client, queue string) { 130 | j := faktory.NewJob("SomeJob", []any{1, "string", 3}) 131 | j.Queue = queue 132 | err := client.Push(j) 133 | if err != nil { 134 | handleError(err) 135 | return 136 | } 137 | } 138 | 139 | func handleError(err error) { 140 | fmt.Println(err.Error()) 141 | } 142 | 143 | func shuffle(src []string) []string { 144 | dest := make([]string, len(src)) 145 | perm := rand.Perm(len(src)) 146 | for i, v := range perm { 147 | dest[v] = src[i] 148 | } 149 | return dest 150 | } 151 | -------------------------------------------------------------------------------- /test/ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem 'faktory_worker_ruby', path: '~/src/fwr' 3 | -------------------------------------------------------------------------------- /test/worker.rb: -------------------------------------------------------------------------------- 1 | require 'faktory' 2 | require 'securerandom' 3 | 4 | =begin 5 | ruby -I~/src/faktory-ruby/lib worker.rb 6 | =end 7 | 8 | # This is the simplest possible Faktory worker process: 9 | # a single threaded loop, processing one job at a time and reporting 10 | # any errors back to the server. 11 | # Notably it doesn't attempt to handle any network errors so 12 | # everything will quickly die if there's a problem. 13 | 14 | def execute(job) 15 | jid = job["jid"] 16 | type = job["jobtype"] 17 | case type 18 | when "failer" 19 | failer(jid, *job['args']) 20 | when "someworker" 21 | someworker(jid, *job['args']) 22 | else 23 | puts "Unknown job type #{type}" 24 | end 25 | end 26 | 27 | def failer(jid, *args) 28 | puts "I am failing, #{jid}" 29 | raise "oops" 30 | end 31 | 32 | def someworker(jid, *args) 33 | puts "Hello, I am #{jid} with args #{args}" 34 | sleep 1 35 | end 36 | 37 | # push a dummy job to Faktory for us to immediately process 38 | $pool = ConnectionPool.new { Faktory::Client.new(url: "tcp://localhost:7419", debug: true) } 39 | $pool.with do |faktory| 40 | puts faktory.push({ queue: :bulk, jobtype: 'failer', jid: SecureRandom.hex(8), args:[1,2,3,"\r\n"] }) 41 | puts faktory.push({ queue: :critical, jobtype: 'someworker', jid: SecureRandom.hex(8), args:[8,2,3,"\r\n"] }) 42 | puts faktory.push({ jobtype: 'someworker', jid: SecureRandom.hex(8), args:[1,2,3,"\r\n"], at: (Time.now.utc + 3600).iso8601 }) 43 | end 44 | 45 | $done = false 46 | 47 | %w(INT TERM).each do |sig| 48 | trap sig do 49 | puts "Shutting down!" 50 | $done = true 51 | end 52 | end 53 | 54 | def heartbeat 55 | loop do 56 | signal = $pool.with {|f| f.beat } 57 | 58 | if signal == "quiet" 59 | puts "No more jobs for me" 60 | elsif signal == "terminate" 61 | puts "Shutting down" 62 | $done = true 63 | return 64 | end 65 | sleep 5 66 | end 67 | end 68 | 69 | def safe_spawn 70 | Thread.new do 71 | begin 72 | yield 73 | rescue 74 | puts $! 75 | p $!.backtrace 76 | end 77 | end 78 | end 79 | 80 | def inker 81 | loop do 82 | $pool.with do |faktory| 83 | puts faktory.push({ queue: :critical, jobtype: 'someworker', jid: SecureRandom.hex(8), args:[26,2,3,"\r\n"] }) 84 | end 85 | sleep(1 + rand) 86 | end 87 | end 88 | 89 | beater = safe_spawn(&method(:heartbeat)) 90 | inker = safe_spawn(&method(:inker)) 91 | 92 | while !$done 93 | job = $pool.with {|f| f.fetch(:critical, :default, :bulk) } 94 | if job 95 | jid = job["jid"] 96 | begin 97 | execute(job) 98 | $pool.with {|f| f.ack(jid) } 99 | rescue => ex 100 | $pool.with {|f| f.fail(jid, ex) } 101 | end 102 | else 103 | sleep 1 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /util/json.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func JsonUnmarshal(data []byte, target any) error { 8 | return json.Unmarshal(data, target) 9 | } 10 | -------------------------------------------------------------------------------- /util/logger.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | // colors. 10 | const ( 11 | red = 31 12 | green = 32 13 | yellow = 33 14 | blue = 34 15 | ) 16 | 17 | type Level int 18 | 19 | const ( 20 | InvalidLevel Level = iota - 1 21 | DebugLevel 22 | InfoLevel 23 | WarnLevel 24 | ErrorLevel 25 | FatalLevel 26 | ) 27 | 28 | var colors = [...]int{ 29 | DebugLevel: green, 30 | InfoLevel: blue, 31 | WarnLevel: yellow, 32 | ErrorLevel: red, 33 | FatalLevel: red, 34 | } 35 | 36 | var lvlPrefix = [...]string{ 37 | DebugLevel: "D", 38 | InfoLevel: "I", 39 | WarnLevel: "W", 40 | ErrorLevel: "E", 41 | FatalLevel: "F", 42 | } 43 | 44 | var ( 45 | LogInfo = false 46 | LogDebug = false 47 | logg = os.Stdout 48 | colorize = isTTY(logg.Fd()) 49 | ) 50 | 51 | const ( 52 | TimeFormat = "2006-01-02T15:04:05.000Z" 53 | ) 54 | 55 | func llog(lvl Level, msg string) { 56 | prefix := lvlPrefix[lvl] 57 | ts := time.Now().UTC().Format(TimeFormat) 58 | 59 | if colorize { 60 | color := colors[lvl] 61 | fmt.Fprintf(logg, "\033[%dm%s\033[0m %s %s\n", color, prefix, ts, msg) 62 | } else { 63 | fmt.Fprintf(logg, "%s %s %s\n", prefix, ts, msg) 64 | } 65 | } 66 | 67 | // 68 | // Logging functions 69 | // 70 | 71 | func InitLogger(level string) { 72 | if level == "info" { 73 | LogInfo = true 74 | } 75 | 76 | if level == "debug" { 77 | LogInfo = true 78 | LogDebug = true 79 | } 80 | } 81 | 82 | func Error(msg string, err error) { 83 | llog(ErrorLevel, fmt.Sprintf("%s: %v", msg, err)) 84 | } 85 | 86 | // Uh oh, not good but not worthy of process death 87 | func Warn(arg string) { 88 | llog(WarnLevel, arg) 89 | } 90 | 91 | func Warnf(msg string, args ...any) { 92 | llog(WarnLevel, fmt.Sprintf(msg, args...)) 93 | } 94 | 95 | // Typical logging output, the default level 96 | func Info(arg string) { 97 | if LogInfo { 98 | llog(InfoLevel, arg) 99 | } 100 | } 101 | 102 | // Typical logging output, the default level 103 | func Infof(msg string, args ...any) { 104 | if LogInfo { 105 | llog(InfoLevel, fmt.Sprintf(msg, args...)) 106 | } 107 | } 108 | 109 | // Verbosity level helps track down production issues: 110 | // 111 | // -l debug 112 | func Debug(arg string) { 113 | if LogDebug { 114 | llog(DebugLevel, arg) 115 | } 116 | } 117 | 118 | // Verbosity level helps track down production issues: 119 | // 120 | // -l debug 121 | func Debugf(msg string, args ...any) { 122 | if LogDebug { 123 | llog(DebugLevel, fmt.Sprintf(msg, args...)) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | cryptorand "crypto/rand" 7 | "encoding/base64" 8 | "fmt" 9 | "math/big" 10 | "os" 11 | "runtime" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | const ( 17 | SIGHUP = 0x1 18 | SIGINT = 0x2 19 | SIGQUIT = 0x3 20 | SIGTERM = 0xF 21 | 22 | maxInt63 = int64(^uint64(0) >> 1) 23 | ) 24 | 25 | func Must[T any](obj T, err error) T { 26 | if err != nil { 27 | panic(err) 28 | } 29 | return obj 30 | } 31 | 32 | var ( 33 | // Set FAKTORY2_PREVIEW=true to enable breaking changes coming in Faktory 2.0. 34 | Faktory2Preview bool = Must(strconv.ParseBool(cmp.Or(os.Getenv("FAKTORY2_PREVIEW"), "false"))) 35 | ) 36 | 37 | func Retryable(ctx context.Context, name string, count int, fn func() error) error { 38 | var err error 39 | for range count { 40 | err = fn() 41 | if err == nil { 42 | return nil 43 | } 44 | select { 45 | case <-ctx.Done(): 46 | return nil 47 | case <-time.After(10 * time.Millisecond): 48 | } 49 | Debugf("Retrying %s due to %v", name, err) 50 | } 51 | return err 52 | } 53 | 54 | func Darwin() bool { 55 | b, _ := FileExists("/Applications") 56 | return b 57 | } 58 | 59 | // FileExists checks if given file exists 60 | func FileExists(path string) (bool, error) { 61 | if _, err := os.Stat(path); err != nil { 62 | if os.IsNotExist(err) { 63 | return false, nil 64 | } 65 | return false, err 66 | } 67 | return true, nil 68 | } 69 | 70 | func RandomJid() string { 71 | bytes := make([]byte, 12) 72 | _, err := cryptorand.Read(bytes) 73 | if err != nil { 74 | panic(fmt.Errorf("unable to read random bytes: %w", err)) 75 | } 76 | 77 | return base64.RawURLEncoding.EncodeToString(bytes) 78 | } 79 | 80 | func RandomInt63() (int64, error) { 81 | r, err := cryptorand.Int(cryptorand.Reader, big.NewInt(maxInt63)) 82 | if err != nil { 83 | return 0, err 84 | } 85 | return r.Int64(), nil 86 | } 87 | 88 | const ( 89 | // This is the canonical timestamp format used by Faktory. 90 | // Always UTC, lexicographically sortable. This is the best 91 | // timestamp format, accept no others. 92 | TimestampFormat = time.RFC3339Nano 93 | ) 94 | 95 | func Thens(tim time.Time) string { 96 | return tim.UTC().Format(TimestampFormat) 97 | } 98 | 99 | func Nows() string { 100 | return time.Now().UTC().Format(TimestampFormat) 101 | } 102 | 103 | func ParseTime(str string) (time.Time, error) { 104 | return time.Parse(TimestampFormat, str) 105 | } 106 | 107 | func MemoryUsageMB() uint64 { 108 | m := runtime.MemStats{} 109 | runtime.ReadMemStats(&m) 110 | mb := m.Sys / 1024 / 1024 111 | return mb 112 | } 113 | 114 | // Backtrace gathers a backtrace for the caller. 115 | // Return a slice of up to N stack frames. 116 | func Backtrace(size int) []string { 117 | pc := make([]uintptr, size) 118 | n := runtime.Callers(2, pc) 119 | if n == 0 { 120 | return []string{} 121 | } 122 | 123 | pc = pc[:n] // pass only valid pcs to runtime.CallersFrames 124 | frames := runtime.CallersFrames(pc) 125 | 126 | str := make([]string, size) 127 | count := 0 128 | 129 | // Loop to get frames. 130 | // A fixed number of pcs can expand to an indefinite number of Frames. 131 | for i := range size { 132 | frame, more := frames.Next() 133 | str[i] = fmt.Sprintf("in %s:%d %s", frame.File, frame.Line, frame.Function) 134 | count++ 135 | if !more { 136 | break 137 | } 138 | } 139 | 140 | return str[0:count] 141 | } 142 | 143 | func DumpProcessTrace() { 144 | buf := make([]byte, 64*1024) 145 | _ = runtime.Stack(buf, true) 146 | Info("FULL PROCESS THREAD DUMP:") 147 | Info(string(buf)) 148 | } 149 | -------------------------------------------------------------------------------- /util/util_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || netbsd || openbsd 2 | 3 | package util 4 | 5 | import ( 6 | "os/exec" 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | func isTTY(fd uintptr) bool { 12 | var termios syscall.Termios 13 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, syscall.TIOCGETA, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) 14 | return err == 0 15 | } 16 | 17 | func EnsureChildShutdown(cmd *exec.Cmd, sig int) { 18 | // This ensures that, on Linux, if Faktory panics, our Redis child process will immediately 19 | // get a SIGTERM signal to shutdown. No such feature on Darwin/BSD, Redis will orphan. 20 | } 21 | -------------------------------------------------------------------------------- /util/util_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package util 4 | 5 | import ( 6 | "os/exec" 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | func isTTY(fd uintptr) bool { 12 | var termios syscall.Termios 13 | // syscall.TCGETS 14 | // https://groups.google.com/forum/#!topic/golang-checkins/kieURujjDEk 15 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, 0x5401, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) 16 | return err == 0 17 | } 18 | 19 | func EnsureChildShutdown(cmd *exec.Cmd, sig int) { 20 | // This ensures that, on Linux, if Faktory panics, our child process will immediately 21 | // get a SIGTERM signal to shutdown. No such feature on Darwin/BSD, child will orphan. 22 | cmd.SysProcAttr = &syscall.SysProcAttr{ 23 | Pdeathsig: syscall.Signal(sig), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestParseTime(t *testing.T) { 13 | // Ruby's iso8601 format: 14 | // require 'time' 15 | // Time.now.utc.iso8601 16 | tm, err := ParseTime("2017-08-17T18:55:26Z") 17 | assert.Nil(t, err) 18 | assert.True(t, tm.Before(time.Now())) 19 | 20 | tm, err = ParseTime("2017-08-17T18:55:26.554544Z") 21 | assert.Nil(t, err) 22 | assert.True(t, tm.Before(time.Now())) 23 | 24 | now := time.Now().UTC() 25 | then, err := ParseTime(Thens(now)) 26 | assert.Nil(t, err) 27 | assert.Equal(t, now, then) 28 | } 29 | 30 | func TestBacktrace(t *testing.T) { 31 | ex := Backtrace(12) 32 | assert.NotNil(t, ex) 33 | assert.True(t, len(ex) > 2) 34 | assert.True(t, len(ex) < 12) 35 | assert.Contains(t, ex[0], "TestBacktrace") 36 | 37 | LogInfo = true 38 | DumpProcessTrace() 39 | } 40 | 41 | func TestLogger(t *testing.T) { 42 | InitLogger("debug") 43 | Debug("hello") 44 | Debugf("hello %s", "mike") 45 | Info("hello") 46 | Infof("hello %s", "mike") 47 | Warn("hello") 48 | Warnf("hello %s", "mike") 49 | Error("hello", os.ErrClosed) 50 | } 51 | 52 | func TestMisc(t *testing.T) { 53 | Darwin() 54 | 55 | ok, err := FileExists("./nope.go") 56 | assert.NoError(t, err) 57 | assert.False(t, ok) 58 | 59 | ok, err = FileExists("./util_test.go") 60 | assert.NoError(t, err) 61 | assert.True(t, ok) 62 | 63 | assert.Equal(t, 16, len(RandomJid())) 64 | 65 | ts := Nows() 66 | assert.True(t, strings.HasPrefix(ts, "20")) 67 | 68 | val := MemoryUsageMB() 69 | assert.True(t, val < 100) 70 | } 71 | -------------------------------------------------------------------------------- /util/util_windows.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os/exec" 5 | ) 6 | 7 | func isTTY(fd uintptr) bool { 8 | // this function controls if we output ANSI coloring to the terminal. 9 | // dunno how to do this on Windows so just play safe and assume it is not a TTY 10 | return false 11 | } 12 | 13 | func EnsureChildShutdown(cmd *exec.Cmd, sig int) { 14 | // This ensures that, on Linux, if Faktory panics, the child process will immediately 15 | // get a signal. Dunno if this is possible on Windows or how it will behave. 16 | } 17 | -------------------------------------------------------------------------------- /webui/dead.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/contribsys/faktory/client" 8 | ) 9 | 10 | func ego_dead(w io.Writer, req *http.Request, key string, dead *client.Job) { 11 | %> 12 | 13 | <% ego_layout(w, req, func() { %> 14 | 15 | <% ego_job_info(w, req, dead) %> 16 | 17 |

<%= t(req, "Error") %>

18 |
19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | <% if dead.Failure.Backtrace != nil { %> 32 | 33 | 34 | 41 | 42 | <% } %> 43 | 44 |
<%= t(req, "ErrorClass") %> 24 | <%= dead.Failure.ErrorType %> 25 |
<%= t(req, "ErrorMessage") %><%= dead.Failure.ErrorMessage %>
<%= t(req, "ErrorBacktrace") %> 35 | 36 | <% for _, line := range dead.Failure.Backtrace { %> 37 | <%= line %>
38 | <% } %> 39 |
40 |
45 |
46 | 47 |
48 | <%== csrfTag(req) %> 49 |
50 | "><%= t(req, "GoBack") %> 51 | 52 | 53 |
54 |
55 | <% }) %> 56 | <% } %> 57 | -------------------------------------------------------------------------------- /webui/debug.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | "runtime" 7 | 8 | "github.com/contribsys/faktory/client" 9 | ) 10 | 11 | func ego_debug(w io.Writer, req *http.Request) { 12 | stats := ctx(req).Store().Stats(req.Context()) 13 | var m runtime.MemStats 14 | runtime.ReadMemStats(&m) 15 | rdata, rtt := redis_info(req) 16 | %> 17 | <% ego_layout(w, req, func() { %> 18 | 19 |

<%= t(req, "Debugging") %>

20 |
21 | 22 | 23 | 24 | 25 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 58 | 59 | 60 | 61 | 65 | 66 | 67 | 71 | 74 | 75 | 76 |
<%= t(req, "Locale") %> 26 | 31 | 32 | Want to help us improve the translations? 33 | Submit a PR. 34 | 35 |
<%= t(req, "Version") %><%= client.Name %> <%= client.Version %>
<%= t(req, "Data Location") %><%= stats["name"] %>
<%= t(req, "Runtime") %>Goroutines: <%= runtime.NumGoroutine() %>, CPUs: <%= runtime.NumCPU() %>
<%= t(req, "Memory") %> 52 | Alloc (KB): <%= m.Alloc / 1024 %>
53 | Live Objects: <%= m.Mallocs - m.Frees %> 54 | <% if amt := client.RssKb(); amt != 0 { %> 55 |
RSS: <%= displayRss(amt) %> 56 | <% } %> 57 |
<%= t(req, "GC") %> 62 | PauseTotal (µs): <%= m.PauseTotalNs / 1000 %>
63 | NumGC: <%= m.NumGC %> 64 |
68 | <%= t(req, "Redis RTT") %> 69 | ? 70 | 72 | <%= rtt %> µs 73 |
77 |
78 | 79 |

<%= t(req, "Redis Info") %>

80 |
81 | <%= rdata %>
82 | 
83 | 84 |

<%= t(req, "Disk Usage") %>

85 |
86 | > df -h
87 | <%= df_h() %>
88 | 
89 | 90 | <% }) %> 91 | <% } %> 92 | -------------------------------------------------------------------------------- /webui/debugging.go: -------------------------------------------------------------------------------- 1 | package webui 2 | 3 | import "os/exec" 4 | 5 | func df_h() string { 6 | cmd := exec.Command("df", "-h") 7 | out, err := cmd.CombinedOutput() 8 | if err != nil { 9 | return err.Error() 10 | } 11 | return string(out) 12 | } 13 | -------------------------------------------------------------------------------- /webui/footer.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | "github.com/contribsys/faktory/client" 7 | ) 8 | 9 | func ego_footer(w io.Writer, req *http.Request) { 10 | %> 11 | 39 | <% } %> 40 | -------------------------------------------------------------------------------- /webui/footer.ego.go: -------------------------------------------------------------------------------- 1 | // Generated by ego. 2 | // DO NOT EDIT 3 | 4 | //line footer.ego:1 5 | 6 | package webui 7 | 8 | import "fmt" 9 | import "html" 10 | import "io" 11 | import "context" 12 | 13 | import ( 14 | "github.com/contribsys/faktory/client" 15 | "net/http" 16 | ) 17 | 18 | func ego_footer(w io.Writer, req *http.Request) { 19 | 20 | //line footer.ego:11 21 | _, _ = io.WriteString(w, "\n\n") 46 | //line footer.ego:39 47 | } 48 | 49 | var _ fmt.Stringer 50 | var _ io.Reader 51 | var _ context.Context 52 | var _ = html.EscapeString 53 | -------------------------------------------------------------------------------- /webui/index.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | "github.com/contribsys/faktory/client" 7 | "github.com/contribsys/faktory/util" 8 | ) 9 | 10 | func ego_index(w io.Writer, req *http.Request) { 11 | ego_layout(w, req, func() { %> 12 | <%== LicenseStatus(w, req) %> 13 | 14 |
15 |

16 | <%= t(req, "Dashboard") %> 17 | 18 | 19 | 20 | 21 |

22 |
23 | <%= t(req, "PollingInterval") %>: 24 | 5 sec 25 |
26 | 27 |
28 |
29 | 30 |
31 |
" data-failed-label="<%= t(req, "Failed") %>">
32 |
33 |
34 | 35 |
36 |
37 | <%= t(req, "History") %> 38 | " class="history-graph <%= daysMatches(req, "7", false) %>"><%= t(req, "OneWeek") %> 39 | " class="history-graph <%= daysMatches(req, "30", true) %>"><%= t(req, "OneMonth") %> 40 | " class="history-graph <%= daysMatches(req, "90", false) %>"><%= t(req, "ThreeMonths") %> 41 | " class="history-graph <%= daysMatches(req, "180", false) %>"><%= t(req, "SixMonths") %> 42 |
43 | 44 |
" data-failed-label="<%= t(req, "Failed") %>" data-processed="<%= processedHistory(req) %>" data-failed="<%= failedHistory(req) %>" data-update-url="<%= relative(req, "/stats") %>">
45 |
46 |
47 | 48 |
49 |
50 |
51 |
52 |

<%= client.Version %>

53 |

<%= t(req, "Version") %>

54 |
55 |
56 | 57 |
58 |
59 |

<%= uptimeInDays(req) %>

60 |

<%= t(req, "Uptime") %>

61 |
62 |
63 | 64 |
65 |
66 |

<%= ctx(req).Server().Stats.Connections %>

67 |

<%= t(req, "Connections") %>

68 |
69 |
70 | 71 |
72 |
73 |

<%= util.MemoryUsageMB() %> MB

74 |

<%= t(req, "MemoryUsage") %>

75 |
76 |
77 | 78 |
79 |
80 |

<%= ctx(req).Server().Stats.Commands %>

81 |

<%= t(req, "CommandsExecuted") %>

82 |
83 |
84 |
85 | <% }) %> 86 | <% // vim: set ft=html %> 87 | <% } %> 88 | -------------------------------------------------------------------------------- /webui/job_info.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/contribsys/faktory/client" 8 | ) 9 | 10 | func ego_job_info(w io.Writer, req *http.Request, job *client.Job) { 11 | %> 12 | 13 |
14 |

<%= t(req, "Job") %>

15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | <% if job.At != "" { %> 46 | 47 | 48 | 49 | 50 | <% } %> 51 | 52 | 53 | 56 | 57 | 58 | 59 | 66 | 67 | <% if job.Custom != nil { %> 68 | 69 | 70 | 75 | 76 | <% } %> 77 | <% if job.Failure != nil { %> 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | <% } %> 95 | 96 |
JID 23 | <%= job.Jid %> 24 |
<%= t(req, "Job") %> 29 | <%= displayJobType(job) %> 30 |
<%= t(req, "Arguments") %> 35 | 36 | 37 |
<%= displayFullArgs(job.Args) %>
38 |
39 |
<%= t(req, "CreatedAt") %><%= relativeTime(job.CreatedAt) %>
<%= t(req, "Scheduled") %><%= relativeTime(job.At) %>
<%= t(req, "Queue") %> 54 | <%= job.Queue %> 55 |
<%= t(req, "Enqueued") %> 60 | <% enq := job.EnqueuedAt; if enq != "" { %> 61 | <%= relativeTime(enq) %> 62 | <% } else { %> 63 | <%= t(req, "NotYetEnqueued") %> 64 | <% } %> 65 |
<%= t(req, "Custom") %> 71 | <% for k, v := range job.Custom { %> 72 | <%= k %>: <%= fmt.Sprintf("%#v", v) %>
73 | <% } %> 74 |
<%= t(req, "RetryCount") %><%= job.Failure.RetryCount %>
<%= t(req, "RetriesRemaining") %><%= job.Failure.RetryRemaining %>
<%= t(req, "OriginallyFailed") %><%= relativeTime(job.Failure.FailedAt) %>
<%= t(req, "NextRetry") %><%= relativeTime(job.Failure.NextAt) %>
97 |
98 | <% } %> 99 | -------------------------------------------------------------------------------- /webui/layout.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | import "net/http" 4 | 5 | func ego_layout(w io.Writer, req *http.Request, yield func()) { 6 | %> 7 | 8 | 9 | 10 | <%= productTitle(req) %> 11 | 12 | "> 13 | " color="#000000"> 14 | "> 15 | "> 16 | "> 17 | 18 | 19 | 20 | <% if rtl(req) { %> 21 | " media="screen" rel="stylesheet" type="text/css"/> 22 | <% } else { %> 23 | " media="screen" rel="stylesheet" type="text/css" /> 24 | <% } %> 25 | 26 | " media="screen" rel="stylesheet" type="text/css" /> 27 | <% if rtl(req) { %> 28 | " media="screen" rel="stylesheet" type="text/css" /> 29 | <% } %> 30 | <%== extraCss(req) %> 31 | 32 | 33 | 34 | 35 | 36 | <% ego_nav(w, req) %> 37 |
38 |
39 |
40 |
41 | <% ego_summary(w, req) %> 42 |
43 | 44 |
45 | <% yield() %> 46 |
47 |
48 |
49 |
50 | <% ego_footer(w, req) %> 51 | 52 | 53 | <% } %> 54 | -------------------------------------------------------------------------------- /webui/morgue.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/contribsys/faktory/client" 8 | "github.com/contribsys/faktory/storage" 9 | ) 10 | 11 | func ego_listDead(w io.Writer, req *http.Request, set storage.SortedSet, count, currentPage uint64) { 12 | totalSize := uint64(set.Size(req.Context())) 13 | %> 14 | 15 | <% ego_layout(w, req, func() { %> 16 | 17 |
18 |
19 |

<%= t(req, "DeadJobs") %>

20 |
21 | <% if totalSize > count { %> 22 |
23 | <% ego_paging(w, req, "/morgue", totalSize, count, currentPage) %> 24 |
25 | <% } %> 26 | <%= filtering("dead") %> 27 |
28 | 29 | <% if totalSize > uint64(0) { %> 30 |
31 | <%== csrfTag(req) %> 32 |
33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | <% setJobs(req, set, count, currentPage, func(idx int, key []byte, job *client.Job) { %> 49 | 50 | 55 | 58 | 61 | 62 | 65 | 70 | 71 | <% }) %> 72 |
37 | 40 | <%= t(req, "LastRetry") %><%= t(req, "Queue") %><%= t(req, "Job") %><%= t(req, "Arguments") %><%= t(req, "Error") %>
51 | 54 | 56 | <%= relativeTime(job.EnqueuedAt) %> 57 | 59 | <%= job.Queue %> 60 | <%= displayJobType(job) %> 63 |
<%= displayArgs(job.Args) %>
64 |
66 | <% if job.Failure != nil { %> 67 |
<%= job.Failure.ErrorType %>: <%= job.Failure.ErrorMessage %>
68 | <% } %> 69 |
73 |
74 |
75 | 76 | 77 |
78 |
79 | 80 | <% if unfiltered() { %> 81 |
82 | <%== csrfTag(req) %> 83 | 84 |
85 | 86 | 87 |
88 |
89 | <% } %> 90 | 91 | <% } else { %> 92 |
<%= t(req, "NoDeadJobsFound") %>
93 | <% } %> 94 | <% }) %> 95 | <% } %> 96 | -------------------------------------------------------------------------------- /webui/nav.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func ego_nav(w io.Writer, req *http.Request) { 10 | %> 11 | 47 | <% } %> 48 | -------------------------------------------------------------------------------- /webui/nav.ego.go: -------------------------------------------------------------------------------- 1 | // Generated by ego. 2 | // DO NOT EDIT 3 | 4 | //line nav.ego:1 5 | 6 | package webui 7 | 8 | import "fmt" 9 | import "html" 10 | import "io" 11 | import "context" 12 | 13 | import ( 14 | "net/http" 15 | "strings" 16 | ) 17 | 18 | func ego_nav(w io.Writer, req *http.Request) { 19 | 20 | //line nav.ego:11 21 | _, _ = io.WriteString(w, "\n
\n
\n
\n \n \n ") 26 | //line nav.ego:16 27 | x := currentStatus(req) 28 | //line nav.ego:17 29 | _, _ = io.WriteString(w, "\n \n \n ") 38 | //line nav.ego:19 39 | _, _ = io.WriteString(w, html.EscapeString(fmt.Sprint(productTitle(req)))) 40 | //line nav.ego:20 41 | _, _ = io.WriteString(w, "\n \n\n \n \n ") 46 | //line nav.ego:24 47 | _, _ = io.WriteString(w, html.EscapeString(fmt.Sprint(t(req, x)))) 48 | //line nav.ego:25 49 | _, _ = io.WriteString(w, "\n \n\n \n\n
\n \n
\n
\n
\n
\n") 91 | //line nav.ego:47 92 | } 93 | 94 | var _ fmt.Stringer 95 | var _ io.Reader 96 | var _ context.Context 97 | var _ = html.EscapeString 98 | -------------------------------------------------------------------------------- /webui/paging.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "math" 6 | "net/http" 7 | ) 8 | 9 | func ego_paging(w io.Writer, req *http.Request, url string, total_size, count, current_page uint64) { 10 | %> 11 | 12 | <% if total_size > count { %> 13 | 34 | <% } %> 35 | <% } %> 36 | -------------------------------------------------------------------------------- /webui/queue.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "encoding/base64" 6 | "net/http" 7 | 8 | "github.com/contribsys/faktory/client" 9 | "github.com/contribsys/faktory/storage" 10 | ) 11 | 12 | func ego_queue(w io.Writer, req *http.Request, q storage.Queue, count, currentPage uint64) { 13 | qs := q.Size(req.Context()) 14 | ego_layout(w, req, func() { %> 15 | 16 |
17 |
18 |

19 | <%= q.Name() %> 20 |

21 |
22 |
23 | <% ego_paging(w, req, fmt.Sprintf("/queues/%s", q.Name()), qs, count, currentPage) %> 24 |
25 |
26 | 27 |
28 | <%== csrfTag(req) %> 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | <% queueJobs(req, q, count, currentPage, func(idx int, key []byte, job *client.Job) { %> 39 | 40 | 41 | 42 | 43 | 44 | 45 | <% }) %> 46 |
<%= t(req, "JID") %><%= t(req, "Type") %><%= t(req, "Arguments") %>
<%= job.Jid %><%= displayJobType(job) %>
<%= displayArgs(job.Args) %>
47 |
48 |
49 |
50 | 51 |
52 |
53 | <% ego_paging(w, req, fmt.Sprintf("/queues/%s", q.Name()), qs, count, currentPage) %> 54 |
55 |
56 |
57 | 58 | <% }) %> 59 | <% } %> 60 | -------------------------------------------------------------------------------- /webui/queues.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import "net/http" 5 | 6 | func ego_listQueues(w io.Writer, req *http.Request) { 7 | %> 8 | 9 | <% ego_layout(w, req, func() { %> 10 | 11 |

<%= t(req, "Queues") %>

12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | <% for _, queue := range queues(req) { %> 21 | 22 | 25 | 26 | 37 | 38 | <% } %> 39 |
<%= t(req, "Queue") %><%= t(req, "Size") %><%= t(req, "Actions") %>
23 | <%= queue.Name %> 24 | <%= uintWithDelimiter(queue.Size) %> 27 |
28 | <%== csrfTag(req) %> 29 | 30 | <% if queue.IsPaused { %> 31 | 32 | <% } else { %> 33 | 34 | <% } %> 35 |
36 |
40 |
41 | 42 | <% }) %> 43 | <% } %> 44 | -------------------------------------------------------------------------------- /webui/retries.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/contribsys/faktory/client" 8 | "github.com/contribsys/faktory/storage" 9 | ) 10 | 11 | func ego_listRetries(w io.Writer, req *http.Request, set storage.SortedSet, count, currentPage uint64) { 12 | totalSize := uint64(set.Size(req.Context())) 13 | %> 14 | 15 | <% ego_layout(w, req, func() { %> 16 | 17 | 18 |
19 |
20 |

<%= t(req, "Retries") %>

21 |
22 | <% if totalSize > count { %> 23 |
24 | <% ego_paging(w, req, "/retries", totalSize, count, currentPage) %> 25 |
26 | <% } %> 27 | <%= filtering("retries") %> 28 |
29 | 30 | <% if totalSize > 0 { %> 31 |
" method="post"> 32 | <%== csrfTag(req) %> 33 |
34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | <% setJobs(req, set, count, currentPage, func(idx int, key []byte, job *client.Job) { %> 51 | 52 | 57 | 60 | 61 | 64 | 65 | 68 | 71 | 72 | <% }) %> 73 |
38 | 41 | <%= t(req, "NextRetry") %><%= t(req, "RetryCount") %><%= t(req, "Queue") %><%= t(req, "Job") %><%= t(req, "Arguments") %><%= t(req, "Error") %>
53 | 56 | 58 | <%= relativeTime(job.Failure.NextAt) %> 59 | <%= job.Failure.RetryCount %> 62 | <%= job.Queue %> 63 | <%= displayJobType(job) %> 66 |
<%= displayArgs(job.Args) %>
67 |
69 |
<%= job.Failure.ErrorType %>: <%= job.Failure.ErrorMessage %>
70 |
74 |
75 |
76 | 77 | 78 | 79 |
80 |
81 | 82 | <% if unfiltered() { %> 83 |
" method="post"> 84 | <%== csrfTag(req) %> 85 | 86 |
87 | 88 | 89 |
90 |
91 | <% } %> 92 | 93 | <% } else { %> 94 |
<%= t(req, "NoRetriesFound") %>
95 | <% } %> 96 | <% }) %> 97 | <% } %> 98 | -------------------------------------------------------------------------------- /webui/retry.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/contribsys/faktory/client" 8 | ) 9 | 10 | func ego_retry(w io.Writer, req *http.Request, key string, retry *client.Job) { 11 | %> 12 | 13 | <% ego_layout(w, req, func() { %> 14 | 15 | <% ego_job_info(w, req, retry) %> 16 | 17 |

<%= t(req, "Error") %>

18 |
19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | <% if retry.Failure.Backtrace != nil { %> 32 | 33 | 34 | 41 | 42 | <% } %> 43 | 44 |
<%= t(req, "ErrorClass") %> 24 | <%= retry.Failure.ErrorType %> 25 |
<%= t(req, "ErrorMessage") %><%= retry.Failure.ErrorMessage %>
<%= t(req, "ErrorBacktrace") %> 35 | 36 | <% for _, line := range retry.Failure.Backtrace { %> 37 | <%= line %>
38 | <% } %> 39 |
40 |
45 |
46 | 47 |
48 | <%== csrfTag(req) %> 49 |
50 | <%= t(req, "GoBack") %> 51 | 52 | 53 |
54 |
55 | <% }) %> 56 | <% } %> 57 | -------------------------------------------------------------------------------- /webui/scheduled.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/contribsys/faktory/client" 8 | "github.com/contribsys/faktory/storage" 9 | ) 10 | 11 | func ego_listScheduled(w io.Writer, req *http.Request, set storage.SortedSet, count, currentPage uint64) { 12 | totalSize := uint64(set.Size(req.Context())) 13 | %> 14 | 15 | <% ego_layout(w, req, func() { %> 16 | 17 |
18 |
19 |

<%= t(req, "ScheduledJobs") %>

20 |
21 | <% if totalSize > 0 && totalSize > count { %> 22 |
23 | <% ego_paging(w, req, "/scheduled", totalSize, count, currentPage) %> 24 |
25 | <% } %> 26 | <%= filtering("scheduled") %> 27 |
28 | 29 | <% if totalSize > 0 { %> 30 | 31 |
32 | <%== csrfTag(req) %> 33 |
34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | <% setJobs(req, set, count, currentPage, func(idx int, key []byte, job *client.Job) { %> 47 | 48 | 51 | 54 | 57 | 58 | 61 | 62 | <% }) %> 63 |
38 | 39 | <%= t(req, "When") %><%= t(req, "Queue") %><%= t(req, "Job") %><%= t(req, "Arguments") %>
49 | 50 | 52 | <%= relativeTime(job.At) %> 53 | 55 | <%= job.Queue %> 56 | <%= displayJobType(job) %> 59 |
<%= displayArgs(job.Args) %>
60 |
64 |
65 |
66 | 67 | 68 |
69 |
70 | <% } else { %> 71 |
<%= t(req, "NoScheduledFound") %>
72 | <% } %> 73 | <% }) %> 74 | <% } %> 75 | -------------------------------------------------------------------------------- /webui/scheduled_job.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import ( 5 | "net/http" 6 | 7 | "github.com/contribsys/faktory/client" 8 | ) 9 | 10 | func ego_scheduled_job(w io.Writer, req *http.Request, key string, job *client.Job) { 11 | ego_layout(w, req, func() { %> 12 | 13 | <% ego_job_info(w, req, job) %> 14 | 15 |
16 | <%== csrfTag(req) %> 17 |
18 | <%= t(req, "GoBack") %> 19 | 20 | 21 |
22 |
23 | 24 | <% }) %> 25 | <% } %> 26 | -------------------------------------------------------------------------------- /webui/scheduled_job.ego.go: -------------------------------------------------------------------------------- 1 | // Generated by ego. 2 | // DO NOT EDIT 3 | 4 | //line scheduled_job.ego:1 5 | 6 | package webui 7 | 8 | import "fmt" 9 | import "html" 10 | import "io" 11 | import "context" 12 | 13 | import ( 14 | "net/http" 15 | 16 | "github.com/contribsys/faktory/client" 17 | ) 18 | 19 | func ego_scheduled_job(w io.Writer, req *http.Request, key string, job *client.Job) { 20 | ego_layout(w, req, func() { 21 | //line scheduled_job.ego:12 22 | _, _ = io.WriteString(w, "\n\n") 23 | //line scheduled_job.ego:13 24 | ego_job_info(w, req, job) 25 | //line scheduled_job.ego:14 26 | _, _ = io.WriteString(w, "\n\n
\n ") 35 | //line scheduled_job.ego:16 36 | _, _ = fmt.Fprint(w, csrfTag(req)) 37 | //line scheduled_job.ego:17 38 | _, _ = io.WriteString(w, "\n
\n ") 43 | //line scheduled_job.ego:18 44 | _, _ = io.WriteString(w, html.EscapeString(fmt.Sprint(t(req, "GoBack")))) 45 | //line scheduled_job.ego:18 46 | _, _ = io.WriteString(w, "\n \n \n
\n
\n\n") 55 | //line scheduled_job.ego:24 56 | }) 57 | //line scheduled_job.ego:25 58 | _, _ = io.WriteString(w, "\n") 59 | //line scheduled_job.ego:25 60 | } 61 | 62 | var _ fmt.Stringer 63 | var _ io.Reader 64 | var _ context.Context 65 | var _ = html.EscapeString 66 | -------------------------------------------------------------------------------- /webui/static/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contribsys/faktory/bfef21eabd53fc8073fcf327decfcbc5cf8947f3/webui/static/img/apple-touch-icon.png -------------------------------------------------------------------------------- /webui/static/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contribsys/faktory/bfef21eabd53fc8073fcf327decfcbc5cf8947f3/webui/static/img/favicon-16x16.png -------------------------------------------------------------------------------- /webui/static/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contribsys/faktory/bfef21eabd53fc8073fcf327decfcbc5cf8947f3/webui/static/img/favicon-32x32.png -------------------------------------------------------------------------------- /webui/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contribsys/faktory/bfef21eabd53fc8073fcf327decfcbc5cf8947f3/webui/static/img/favicon.ico -------------------------------------------------------------------------------- /webui/static/img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logo Copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /webui/static/img/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contribsys/faktory/bfef21eabd53fc8073fcf327decfcbc5cf8947f3/webui/static/img/status.png -------------------------------------------------------------------------------- /webui/static/locales/ar.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | ar: 3 | TextDirection: rtl 4 | Dashboard: لوحة التحكم 5 | Status: حالة 6 | Time: وقت 7 | Namespace: مساحة الاسم 8 | Realtime: الزمن الفعلي 9 | History: تاريخ 10 | Busy: مشغول 11 | Processed: تمت المعالجة 12 | Failed: فشل 13 | Scheduled: مجدول 14 | Retries: إعادة محاولة 15 | Enqueued: في الرتل 16 | Worker: عامل 17 | LivePoll: استعلام مباشر 18 | StopPolling: إيقاف الاستعلامات 19 | Queue: رتل 20 | Class: نوع 21 | Job: وظيفة 22 | Arguments: مدخلات 23 | Extras: إضافات 24 | Started: بدأت 25 | ShowAll: عرض الكل 26 | CurrentMessagesInQueue: الرسائل الحالية في الرتل %{queue} 27 | AddToQueue: إضافة إلى الرتل 28 | AreYouSureDeleteJob: هل أنت متأكد من حذف الوظيفة؟ 29 | AreYouSureDeleteQueue: هل أنت متأكد من حذف الرتل %{queue}؟ 30 | Delete: حذف 31 | Queues: أرتال 32 | Size: حجم 33 | Actions: إجراءات 34 | NextRetry: إعادة المحاولة القادمة 35 | RetryCount: عدد مرات إعادة المحاولة 36 | RetryNow: إعادة المحاولة الآن 37 | Kill: إبطال 38 | LastRetry: إعادة المحاولة الأخيرة 39 | OriginallyFailed: فشل أصلا 40 | AreYouSure: هل انت متأكد؟ 41 | DeleteAll: حذف الكل 42 | RetryAll: إعادة المحاولة للكل 43 | NoRetriesFound: لاتوجد أي إعادة محاولة 44 | Error: خطأ 45 | ErrorClass: نوع الخطأ 46 | ErrorMessage: رسالة الخطأ 47 | ErrorBacktrace: تتبع الخطأ 48 | GoBack: إلى الخلف 49 | NoScheduledFound: لايوجد وظائف مجدولة 50 | When: متى 51 | ScheduledJobs: وظائف مجدولة 52 | idle: خامل 53 | active: نشيط 54 | Version: إصدار 55 | Connections: اتصالات 56 | MemoryUsage: استخدام الذاكرة 57 | PeakMemoryUsage: ذروة استخدام الذاكرة 58 | Uptime: زمن العمل 59 | OneWeek: أسبوع 60 | OneMonth: شهر 61 | ThreeMonths: ثلاثة أشهر 62 | SixMonths: ستة أشهر 63 | Failures: فشل 64 | DeadJobs: وظائف ميتة 65 | NoDeadJobsFound: لاتوجد وظائف ميتة 66 | Dead: ميتة 67 | Processes: عمليات 68 | Thread: نيسب 69 | Threads: نياسب 70 | Jobs: وظائف 71 | Paused: إيقاف مؤقت 72 | Stop: إيقاف 73 | Quiet: هدوء 74 | StopAll: إيقاف الكل 75 | QuietAll: هدوء الكل 76 | PollingInterval: الفاصل الزمني بين الاستعلامات 77 | Plugins: الإضافات 78 | NotYetEnqueued: لم تدخل الرتل بعد 79 | CreatedAt: أنشئت في 80 | BackToApp: العودة إلى التطبيق 81 | -------------------------------------------------------------------------------- /webui/static/locales/cs.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | cs: 3 | Dashboard: Kontrolní panel 4 | Status: Stav 5 | Time: Čas 6 | Namespace: Jmenný prostor 7 | Realtime: V reálném čase 8 | History: Historie 9 | Busy: Zaneprázdněné 10 | Processed: Zpracované 11 | Failed: Nezdařené 12 | Scheduled: Naplánované 13 | Retries: Opakování 14 | Enqueued: Zařazené 15 | Worker: Worker 16 | LivePoll: Průběžně aktualizovat 17 | StopPolling: Zastavit průběžnou aktualizaci 18 | Queue: Fronta 19 | Class: Třída 20 | Job: Úkol 21 | Arguments: Argumenty 22 | Extras: Doplňky 23 | Started: Začal 24 | ShowAll: Ukázat všechny 25 | CurrentMessagesInQueue: Aktuální úkoly ve frontě %{queue} 26 | Delete: Odstranit 27 | AddToQueue: Přidat do fronty 28 | AreYouSureDeleteJob: Jste si jisti, že chcete odstranit tento úkol? 29 | AreYouSureDeleteQueue: Jste si jisti, že chcete odstranit frontu %{queue}? 30 | Queues: Fronty 31 | Size: Velikost 32 | Actions: Akce 33 | NextRetry: Další opakování 34 | RetryCount: Počet opakování 35 | RetryNow: Opakovat teď 36 | Kill: Zabít 37 | LastRetry: Poslední opakování 38 | OriginallyFailed: Původně se nezdařilo 39 | AreYouSure: Jste si jisti? 40 | DeleteAll: Odstranit vše 41 | RetryAll: Opakovat vše 42 | NoRetriesFound: Nebyla nalezena žádná opakování 43 | Error: Chyba 44 | ErrorClass: Třída chyby 45 | ErrorMessage: Chybová zpráva 46 | ErrorBacktrace: Chybový výstup 47 | GoBack: ← Zpět 48 | NoScheduledFound: Nebyly nalezeny žádné naplánované úkoly 49 | When: Kdy 50 | ScheduledJobs: Naplánované úkoly 51 | idle: nečinný 52 | active: aktivní 53 | Version: Verze 54 | Connections: Připojení 55 | MemoryUsage: Využití paměti 56 | PeakMemoryUsage: Nejvyšší využití paměti 57 | Uptime: Uptime (dny) 58 | OneWeek: 1 týden 59 | OneMonth: 1 měsíc 60 | ThreeMonths: 3 měsíce 61 | SixMonths: 6 měsíců 62 | Failures: Selhání 63 | DeadJobs: Mrtvé úkoly 64 | NoDeadJobsFound: Nebyly nalezeny žádné mrtvé úkoly 65 | Dead: Mrtvé 66 | Processes: Procesy 67 | Thread: Vlákno 68 | Threads: Vlákna 69 | Jobs: Úkoly 70 | Paused: Pozastavené 71 | Stop: Zastavit 72 | Quiet: Ztišit 73 | StopAll: Zastavit vše 74 | QuietAll: Ztišit vše 75 | PollingInterval: Interval obnovení 76 | Plugins: Doplňky 77 | NotYetEnqueued: Ještě nezařazeno 78 | CreatedAt: Vytvořeno 79 | -------------------------------------------------------------------------------- /webui/static/locales/da.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | da: 3 | Dashboard: Instrumentbræt 4 | Status: Status 5 | Time: Tid 6 | Namespace: Namespace 7 | Realtime: Real-time 8 | History: Historik 9 | Busy: Travl 10 | Processed: Processeret 11 | Failed: Fejlet 12 | Scheduled: Planlagt 13 | Retries: Forsøg 14 | Enqueued: I kø 15 | Worker: Arbejder 16 | LivePoll: Live Poll 17 | StopPolling: Stop Polling 18 | Queue: Kø 19 | Class: Klasse 20 | Job: Job 21 | Arguments: Argumenter 22 | Extras: Ekstra 23 | Started: Startet 24 | ShowAll: Vis alle 25 | CurrentMessagesInQueue: Nuværende beskeder i %{queue} 26 | Delete: Slet 27 | AddToQueue: Tilføj til kø 28 | AreYouSureDeleteJob: Er du sikker på at du vil slette dette job? 29 | AreYouSureDeleteQueue: Er du sikker på at du vil slette %{queue} køen? 30 | Queues: Køer 31 | Size: Størrelse 32 | Actions: Actions 33 | NextRetry: Næste forsøg 34 | RetryCount: Antal forsøg 35 | RetryNow: Prøv igen nu 36 | LastRetry: Sidste forsøg 37 | OriginallyFailed: Oprindeligt fejlet 38 | AreYouSure: Er du sikker? 39 | DeleteAll: Slet alle 40 | RetryAll: Forsøg alle 41 | NoRetriesFound: Ingen gen-forsøg var fundet 42 | Error: Fejl 43 | ErrorClass: Fejl klasse 44 | ErrorMessage: Fejl besked 45 | ErrorBacktrace: Fejl backtrace 46 | GoBack: ← Tilbage 47 | NoScheduledFound: Ingen jobs i kø fundet 48 | When: Når 49 | ScheduledJobs: Jobs i kø 50 | idle: idle 51 | active: aktiv 52 | Version: Version 53 | Connections: Forbindelser 54 | MemoryUsage: RAM forbrug 55 | PeakMemoryUsage: Peak RAM forbrug 56 | Uptime: Oppetid (dage) 57 | OneWeek: 1 uge 58 | OneMonth: 1 måned 59 | ThreeMonths: 3 måneder 60 | SixMonths: 6 måneder 61 | Failures: Fejl 62 | DeadJobs: Døde jobs 63 | NoDeadJobsFound: Ingen døde jobs fundet 64 | Dead: Død 65 | Processes: Processer 66 | Thread: Tråd 67 | Threads: Tråde 68 | Jobs: Jobs 69 | -------------------------------------------------------------------------------- /webui/static/locales/de.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | de: 3 | Dashboard: Dashboard 4 | Status: Status 5 | Time: Zeit 6 | Namespace: Namensraum 7 | Realtime: Echtzeit 8 | History: Verlauf 9 | Busy: Beschäftigt 10 | Processed: Verarbeitet 11 | Failed: Fehlgeschlagen 12 | Scheduled: Geplant 13 | Retries: Versuche 14 | Enqueued: In der Warteschlange 15 | Worker: Arbeiter 16 | LivePoll: Live Poll 17 | StopPolling: Abfrage stoppen 18 | Queue: Warteschlange 19 | Class: Klasse 20 | Job: Job 21 | Extras: Extras 22 | Arguments: Argumente 23 | Started: Gestartet 24 | ShowAll: Alle anzeigen 25 | CurrentMessagesInQueue: Aktuelle Nachrichten in %{queue} 26 | Delete: Löschen 27 | AddToQueue: In Warteschlange einreihen 28 | AreYouSureDeleteJob: Möchtest du diesen Job wirklich löschen? 29 | AreYouSureDeleteQueue: Möchtest du %{queue} wirklich löschen? 30 | Queues: Warteschlangen 31 | Size: Größe 32 | Actions: Aktionen 33 | NextRetry: Nächster Versuch 34 | RetryCount: Anzahl der Versuche 35 | RetryNow: Jetzt erneut versuchen 36 | Kill: Töten 37 | LastRetry: Letzter Versuch 38 | OriginallyFailed: Ursprünglich fehlgeschlagen 39 | AreYouSure: Bist du sicher? 40 | DeleteAll: Alle löschen 41 | RetryAll: Alle erneut versuchen 42 | NoRetriesFound: Keine erneuten Versuche gefunden 43 | Error: Fehler 44 | ErrorClass: Fehlerklasse 45 | ErrorMessage: Fehlernachricht 46 | ErrorBacktrace: Fehlerbericht 47 | GoBack: ← Zurück 48 | NoScheduledFound: Keine geplanten Jobs gefunden 49 | When: Wann 50 | ScheduledJobs: Jobs in der Warteschlange 51 | idle: untätig 52 | active: aktiv 53 | Version: Version 54 | Connections: Verbindungen 55 | MemoryUsage: RAM-Nutzung 56 | PeakMemoryUsage: Maximale RAM-Nutzung 57 | Uptime: Laufzeit 58 | OneWeek: 1 Woche 59 | OneMonth: 1 Monat 60 | ThreeMonths: 3 Monate 61 | SixMonths: 6 Monate 62 | Failures: Ausfälle 63 | DeadJobs: Gestorbene Jobs 64 | NoDeadJobsFound: Keine toten Jobs gefunden 65 | Dead: Tot 66 | Processes: Prozesse 67 | Thread: Thread 68 | Threads: Threads 69 | Jobs: Jobs 70 | -------------------------------------------------------------------------------- /webui/static/locales/el.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | el: # <---- change this to your locale code 3 | Dashboard: Πίνακας Ελέγχου 4 | Status: Κατάσταση 5 | Time: Χρόνος 6 | Namespace: Namespace 7 | Realtime: Τρέχουσα Κατάσταση 8 | History: Ιστορικό 9 | Busy: Απασχολημένο 10 | Processed: Επεξεργάστηκε 11 | Failed: Απέτυχε 12 | Scheduled: Προγραματίστηκε 13 | Retries: Προσπάθειες 14 | Enqueued: Μπήκαν στην στοίβα 15 | Worker: Εργάτης 16 | LivePoll: Τρέχουσα Κατάσταση 17 | StopPolling: Διακοπή Τρέχουσας Κατάστασης 18 | Queue: Στοίβα 19 | Class: Κλάση 20 | Job: Εργασία 21 | Arguments: Ορίσματα 22 | Extras: Extras 23 | Started: Ξεκίνησαν 24 | ShowAll: Εμφάνιση Όλων 25 | CurrentMessagesInQueue: Τρέχουσες εργασίες %{queue} 26 | Delete: Διαγραφή 27 | AddToQueue: Προσθήκη στην στοίβα 28 | AreYouSureDeleteJob: Θέλετε να διαγράψετε την εργασία αυτη; 29 | AreYouSureDeleteQueue: Θέλετε να διαγράψετε την %{queue} στοίβα? 30 | Queues: Στοίβες 31 | Size: Μέγεθος 32 | Actions: Ενέργειες 33 | NextRetry: Επόμενη προσπάθεια 34 | RetryCount: Αριθμός προσπαθειών 35 | RetryNow: Προσπάθησε τώρα 36 | LastRetry: Τελευταία προσπάθεια 37 | OriginallyFailed: Αρχικές Αποτυχίες 38 | AreYouSure: Είστε σίγουρος? 39 | DeleteAll: Διαγραφή όλων 40 | RetryAll: Επανάληψη Όλων 41 | NoRetriesFound: Δεν βρέθηκαν προσπάθειες 42 | Error: Σφάλμα 43 | ErrorClass: Κλάση σφάλματος 44 | ErrorMessage: Μήνυμα Σφάλματος 45 | ErrorBacktrace: Σφάλμα Backtrace 46 | GoBack: ← Πίσω 47 | NoScheduledFound: Δεν βρέθηκαν προγραμματισμένες εργασίες 48 | When: Πότε 49 | ScheduledJobs: Προγραμματισμένες Εργασίες 50 | idle: αδρανής 51 | active: ενεργή 52 | Version: Έκδοση 53 | Connections: Συνδέσεις 54 | MemoryUsage: Χρήση Μνήμης 55 | PeakMemoryUsage: Μέγιστη Χρήση Μνήμης 56 | Uptime: Διάρκεια Λειτουργείας (ημέρες) 57 | OneWeek: 1 εβδομάδα 58 | OneMonth: 1 μήνας 59 | ThreeMonths: 3 μήνες 60 | SixMonths: 6 μήνες 61 | Failures: Αποτυχίες 62 | DeadJobs: Αδρανείς Εργασίες 63 | NoDeadJobsFound: Δεν βρέθηκαν αδρανείς εργασίες 64 | Dead: Αδρανείς 65 | Processes: Διεργασίες 66 | Thread: Νήμα 67 | Threads: Νήματα 68 | Jobs: Εργασίες 69 | -------------------------------------------------------------------------------- /webui/static/locales/en.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | en: # <---- change this to your locale code 3 | Dashboard: Dashboard 4 | Status: Status 5 | Time: Time 6 | Namespace: Namespace 7 | Realtime: Real-time 8 | History: History 9 | Busy: Busy 10 | Processed: Processed 11 | Failed: Failed 12 | Scheduled: Scheduled 13 | Retries: Retries 14 | Enqueued: Enqueued 15 | Worker: Worker 16 | LivePoll: Live Poll 17 | StopPolling: Stop Polling 18 | Queue: Queue 19 | Class: Class 20 | Job: Job 21 | Arguments: Arguments 22 | Extras: Extras 23 | Started: Started 24 | ShowAll: Show All 25 | CurrentMessagesInQueue: Current jobs in %{queue} 26 | Delete: Delete 27 | ClearQueue: Clear 28 | AddToQueue: Add to queue 29 | AreYouSureDeleteJob: Are you sure you want to delete this job? 30 | AreYouSureDeleteQueue: Are you sure you want to delete the %{queue} queue? 31 | Queues: Queues 32 | Size: Size 33 | Actions: Actions 34 | NextRetry: Next Retry 35 | RetryCount: Retry Count 36 | RetryNow: Retry Now 37 | Kill: Kill 38 | LastRetry: Last Retry 39 | OriginallyFailed: Originally Failed 40 | AreYouSure: Are you sure? 41 | DeleteAll: Delete All 42 | RetryAll: Retry All 43 | NoRetriesFound: No retries were found 44 | Error: Error 45 | ErrorClass: Error Class 46 | ErrorMessage: Error Message 47 | ErrorBacktrace: Error Backtrace 48 | GoBack: ← Back 49 | NoScheduledFound: No scheduled jobs were found 50 | When: When 51 | ScheduledJobs: Scheduled Jobs 52 | idle: idle 53 | active: active 54 | Version: Version 55 | Connections: Connections 56 | MemoryUsage: Memory Usage 57 | PeakMemoryUsage: Peak Memory Usage 58 | Uptime: Uptime (days) 59 | OneWeek: 1 week 60 | OneMonth: 1 month 61 | ThreeMonths: 3 months 62 | SixMonths: 6 months 63 | Failures: Failures 64 | DeadJobs: Dead Jobs 65 | NoDeadJobsFound: No dead jobs were found 66 | Dead: Dead 67 | Processes: Processes 68 | Thread: Thread 69 | Threads: Threads 70 | Jobs: Jobs 71 | Paused: Paused 72 | Stop: Stop 73 | Quiet: Quiet 74 | StopAll: Stop All 75 | QuietAll: Quiet All 76 | PollingInterval: Polling interval 77 | Plugins: Plugins 78 | NotYetEnqueued: Not yet enqueued 79 | CreatedAt: Created At 80 | BackToApp: Back to App 81 | CommandsExecuted: Commands Executed 82 | Pause: Pause 83 | Resume: Resume 84 | RetriesRemaining: Retries Remaining 85 | -------------------------------------------------------------------------------- /webui/static/locales/es.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | es: 3 | Dashboard: Panel de Control 4 | Status: Estatus 5 | Time: Tiempo 6 | Namespace: Espacio de Nombre 7 | Realtime: Tiempo Real 8 | History: Historial 9 | Busy: Ocupado 10 | Processed: Procesadas 11 | Failed: Fallidas 12 | Scheduled: Programadas 13 | Retries: Reintentos 14 | Enqueued: En Fila 15 | Worker: Trabajador 16 | LivePoll: Sondeo en Vivo 17 | StopPolling: Detener Sondeo 18 | Queue: Fila 19 | Class: Clase 20 | Job: Trabajo 21 | Arguments: Argumentos 22 | Extras: Extras 23 | Started: Hora de Inicio 24 | ShowAll: Mostrar Todo 25 | CurrentMessagesInQueue: Mensajes actualmente en %{queue} 26 | Delete: Eliminar 27 | AddToQueue: Añadir a fila 28 | AreYouSureDeleteJob: ¿Estás seguro de eliminar este trabajo? 29 | AreYouSureDeleteQueue: ¿Estás seguro de eliminar la fila %{queue}? 30 | Queues: Filas 31 | Size: Tamaño 32 | Actions: Acciones 33 | NextRetry: Siguiente Intento 34 | RetryCount: Numero de Reintentos 35 | RetryNow: Reintentar Ahora 36 | Kill: Matar 37 | LastRetry: Último Reintento 38 | OriginallyFailed: Falló Originalmente 39 | AreYouSure: ¿Estás seguro? 40 | DeleteAll: Borrar Todo 41 | RetryAll: Reintentar Todo 42 | NoRetriesFound: No se encontraron reintentos 43 | Error: Error 44 | ErrorClass: Clase del Error 45 | ErrorMessage: Mensaje de Error 46 | ErrorBacktrace: Trazado del Error 47 | GoBack: ← Regresar 48 | NoScheduledFound: No se encontraron trabajos pendientes 49 | When: Cuando 50 | ScheduledJobs: Trabajos programados 51 | idle: inactivo 52 | active: activo 53 | Version: Versión 54 | Connections: Conexiones 55 | MemoryUsage: Uso de Memoria 56 | PeakMemoryUsage: Máximo Uso de Memoria 57 | Uptime: Tiempo de Funcionamiento (días) 58 | OneWeek: 1 semana 59 | OneMonth: 1 mes 60 | ThreeMonths: 3 meses 61 | SixMonths: 6 meses 62 | Failures: Fallas 63 | DeadJobs: Trabajos muertos 64 | NoDeadJobsFound: No hay trabajos muertos 65 | Dead: Muerto 66 | Processes: Procesos 67 | Thread: Hilo 68 | Threads: Hilos 69 | Jobs: Trabajos 70 | -------------------------------------------------------------------------------- /webui/static/locales/fa.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | fa: # <---- change this to your locale code 3 | TextDirection: rtl 4 | Dashboard: داشبورد 5 | Status: اعلان 6 | Time: رمان 7 | Namespace: فضای نام 8 | Realtime: زنده 9 | History: تاریخچه 10 | Busy: مشغول 11 | Processed: پردازش شده 12 | Failed: ناموفق 13 | Scheduled: زمان بندی 14 | Retries: تکرار 15 | Enqueued: صف بندی نشدند 16 | Worker: کارگزار 17 | LivePoll: Live Poll 18 | StopPolling: Stop Polling 19 | Queue: صف 20 | Class: کلاس 21 | Job: کار 22 | Arguments: آرگومنت 23 | Extras: اضافی 24 | Started: شروع شده 25 | ShowAll: نمایش همه 26 | CurrentMessagesInQueue: کار فعلی در %{queue} 27 | Delete: حذف 28 | AddToQueue: افزودن به صف 29 | AreYouSureDeleteJob: آیا شما مطمعن هستید از حذف این کار ؟ 30 | AreYouSureDeleteQueue: ایا شما مطمعنید از حذف %{queue} ? 31 | Queues: صف ها 32 | Size: سایز 33 | Actions: اعمال 34 | NextRetry: بار دیگر تلاش کنید 35 | RetryCount: تعداد تلاش ها 36 | RetryNow: تلاش مجدد 37 | Kill: کشتن 38 | LastRetry: آخرین تلاش 39 | OriginallyFailed: Originally Failed 40 | AreYouSure: آیا مطمعن هستید? 41 | DeleteAll: حذف همه 42 | RetryAll: تلاش مجدد برای همه 43 | NoRetriesFound: هیچ تلاش پیدا نشد 44 | Error: خطا 45 | ErrorClass: خطا کلاس 46 | ErrorMessage: پیغام خطا 47 | ErrorBacktrace: خطای معکوس 48 | GoBack: ← برگشت 49 | NoScheduledFound: هیچ کار برنامه ریزی شده ای یافت نشد 50 | When: وقتی که 51 | ScheduledJobs: کار برنامه ریزی شده 52 | idle: بیهودی 53 | active: فعال 54 | Version: ورژن 55 | Connections: ارتباطات 56 | MemoryUsage: حافظه استفاده شده 57 | PeakMemoryUsage: اوج حافظه استفاده شده 58 | Uptime: آپ تایم (روز) 59 | OneWeek: ۱ هفته 60 | OneMonth: ۱ ماه 61 | ThreeMonths: ۳ ماه 62 | SixMonths: ۶ ماه 63 | Failures: شکست ها 64 | DeadJobs: کار مرده 65 | NoDeadJobsFound: کار مرده ای یافت نشد 66 | Dead: مرده 67 | Processes: پردازش ها 68 | Thread: رشته 69 | Threads: رشته ها 70 | Jobs: کار ها 71 | Paused: مکث 72 | Stop: توقف 73 | Quiet: خروج 74 | StopAll: توقف همه 75 | QuietAll: خروج همه 76 | PollingInterval: Polling interval 77 | Plugins: پلاگین ها 78 | NotYetEnqueued: بدون صف بندی 79 | CreatedAt: ساخته شده در 80 | BackToApp: برگشت به برنامه 81 | -------------------------------------------------------------------------------- /webui/static/locales/fr.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | fr: 3 | Dashboard: Tableau de Bord 4 | Status: État 5 | Time: Heure 6 | Namespace: Namespace 7 | Realtime: Temps réel 8 | History: Historique 9 | Busy: En cours 10 | Processed: Traitées 11 | Failed: Échouées 12 | Scheduled: Planifiées 13 | Retries: Tentatives 14 | Enqueued: En attente 15 | Worker: Travailleur 16 | LivePoll: Temps réel 17 | StopPolling: Arrêt du temps réel 18 | Queue: Queue 19 | Class: Classe 20 | Job: Tâche 21 | Arguments: Arguments 22 | Extras: Extras 23 | Started: Démarrée 24 | ShowAll: Tout montrer 25 | CurrentMessagesInQueue: Messages actuellement dans %{queue} 26 | Delete: Supprimer 27 | AddToQueue: Ajouter à la queue 28 | AreYouSureDeleteJob: Êtes-vous certain de vouloir supprimer cette tâche ? 29 | AreYouSureDeleteQueue: Êtes-vous certain de vouloir supprimer la queue %{queue} ? 30 | Queues: Queues 31 | Size: Taille 32 | Actions: Actions 33 | NextRetry: Prochain essai 34 | RetryCount: Nombre d'essais 35 | RetryNow: Réessayer maintenant 36 | Kill: Tuer 37 | LastRetry: Dernier essai 38 | OriginallyFailed: Échec initial 39 | AreYouSure: Êtes-vous certain ? 40 | DeleteAll: Tout supprimer 41 | RetryAll: Tout réessayer 42 | NoRetriesFound: Aucune tâche à réessayer n’a été trouvée 43 | Error: Erreur 44 | ErrorClass: Classe d’erreur 45 | ErrorMessage: Message d’erreur 46 | ErrorBacktrace: Backtrace d’erreur 47 | GoBack: ← Retour 48 | NoScheduledFound: Aucune tâche planifiée n'a été trouvée 49 | When: Quand 50 | ScheduledJobs: Tâches planifiées 51 | idle: inactif 52 | active: actif 53 | Version: Version 54 | Connections: Connexions 55 | MemoryUsage: Mémoire utilisée 56 | PeakMemoryUsage: Mémoire utilisée (max.) 57 | Uptime: Uptime (jours) 58 | OneWeek: 1 semaine 59 | OneMonth: 1 mois 60 | ThreeMonths: 3 mois 61 | SixMonths: 6 mois 62 | Failures: Echecs 63 | DeadJobs: Tâches mortes 64 | NoDeadJobsFound: Aucune tâche morte n'a été trouvée 65 | Dead: Mortes 66 | Processes: Processus 67 | Thread: Thread 68 | Threads: Threads 69 | Jobs: Tâches 70 | Paused: Mise en pause 71 | Stop: Arrêter 72 | Quiet: Clôturer 73 | StopAll: Tout arrêter 74 | QuietAll: Tout clôturer 75 | PollingInterval: Interval de rafraîchissement 76 | Plugins: Plugins 77 | NotYetEnqueued: Pas encore en file d'attente 78 | CreatedAt: Créée le 79 | -------------------------------------------------------------------------------- /webui/static/locales/he.yml: -------------------------------------------------------------------------------- 1 | he: 2 | TextDirection: rtl 3 | Dashboard: לוח מחוונים 4 | Status: מצב 5 | Time: שעה 6 | Namespace: מרחב שם 7 | Realtime: זמן אמת 8 | History: היסטוריה 9 | Busy: עסוקים 10 | Processed: עובדו 11 | Failed: נכשלו 12 | Scheduled: מתוכננים 13 | Retries: נסיונות חוזרים 14 | Enqueued: בתור 15 | Worker: עובד 16 | LivePoll: תשאול חי 17 | StopPolling: עצור תשאול 18 | Queue: תור 19 | Class: מחלקה 20 | Job: עבודה 21 | Arguments: ארגומנטים 22 | Extras: תוספות 23 | Started: הותחלו 24 | ShowAll: הצג את הכל 25 | CurrentMessagesInQueue: עבודות נוכחיות בתור %{queue} 26 | Delete: מחק 27 | AddToQueue: הוסף לתור 28 | AreYouSureDeleteJob: האם אתם בטוחים שברצונכם למחוק את העבודה הזאת? 29 | AreYouSureDeleteQueue: האם אתם בטוחים שברצונכם למחוק את התור %{queue}? 30 | Queues: תורים 31 | Size: אורך 32 | Actions: פעולות 33 | NextRetry: ניסיון חוזר הבא 34 | RetryCount: מספר נסיונות חוזרים 35 | RetryNow: נסה שוב עכשיו 36 | Kill: הרוג 37 | LastRetry: ניסיון חוזר אחרון 38 | OriginallyFailed: נכשל בניסיון הראשון 39 | AreYouSure: אתם בטוחים? 40 | DeleteAll: מחק הכל 41 | RetryAll: נסה שוב את הכל 42 | NoRetriesFound: לא נמצאו נסיונות חוזרים 43 | Error: שגיאה 44 | ErrorClass: סוג השגיאה 45 | ErrorMessage: הודעת השגיאה 46 | ErrorBacktrace: מעקב לאחור של השגיאה 47 | GoBack: ← אחורה 48 | NoScheduledFound: לא נמצאו עבודות מתוכננות 49 | When: מתי 50 | ScheduledJobs: עבודות מתוכננות 51 | idle: במנוחה 52 | active: פעיל 53 | Version: גירסה 54 | Connections: חיבורים 55 | MemoryUsage: שימוש בזיכרון 56 | PeakMemoryUsage: שיא השימוש בזיכרון 57 | Uptime: זמן פעילות (ימים) 58 | OneWeek: שבוע 1 59 | OneMonth: חודש 1 60 | ThreeMonths: 3 חדשים 61 | SixMonths: 6 חדשים 62 | Failures: כשלונות 63 | DeadJobs: עבודות מתות 64 | NoDeadJobsFound: לא נמצאו עבודות מתות 65 | Dead: מתים 66 | Processes: תהליכים 67 | Thread: חוט 68 | Threads: חוטים 69 | Jobs: עבודות 70 | Paused: הופסקו 71 | Stop: עצור 72 | Quiet: שקט 73 | StopAll: עצור הכל 74 | QuietAll: השקט את כולם 75 | PollingInterval: מרווח זמן בין תשאולים 76 | Plugins: תוספים 77 | NotYetEnqueued: עוד לא בתור 78 | CreatedAt: נוצר ב 79 | BackToApp: חזרה לאפליקציה 80 | -------------------------------------------------------------------------------- /webui/static/locales/hi.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | hi: 3 | Dashboard: डैशबोर्ड 4 | Status: स्थिती 5 | Time: समय 6 | Namespace: नामस्थान 7 | Realtime: रिअल टाईम 8 | History: वृत्तान्त 9 | Busy: व्यस्थ 10 | Processed: कार्रवाई कृत 11 | Failed: असफल 12 | Scheduled: परिगणित 13 | Retries: पुनर्प्रयास 14 | Enqueued: कतारबद्ध 15 | Worker: वर्कर 16 | LivePoll: लाईव सर्वेक्षण 17 | StopPolling: सर्वेक्षण रोको 18 | Queue: कतार 19 | Class: क्लास 20 | Job: कार्य 21 | Arguments: अर्गुमेन्ट्स् 22 | Extras: अतिरिक्त 23 | Started: शुरु हुआ 24 | ShowAll: सब दिखाएं 25 | CurrentMessagesInQueue: %{queue} कतार मे वर्तमान कार्य 26 | Delete: हटाओ 27 | AddToQueue: कतार मे जोड़ें 28 | AreYouSureDeleteJob: क्या आप इस कार्य को हटाना चाहते है? 29 | AreYouSureDeleteQueue: क्या आप %{queue} कतार को हटाना चाहते है? 30 | Queues: कतारे 31 | Size: आकार 32 | Actions: कार्रवाई 33 | NextRetry: अगला पुन:प्रयास 34 | RetryCount: पुन:प्रयास संख्या 35 | RetryNow: पुन:प्रयास करे 36 | Kill: नष्ट करे 37 | LastRetry: अंतिम पुन:प्रयास 38 | OriginallyFailed: पहिले से विफल 39 | AreYouSure: क्या आपको यकीन है? 40 | DeleteAll: सब हटाओ 41 | RetryAll: सब पुन:प्रयास करे 42 | NoRetriesFound: कोई पुनर्प्रयास नही पाए गए 43 | Error: एरर 44 | ErrorClass: एरर क्लास 45 | ErrorMessage: एरर संदेश 46 | ErrorBacktrace: एरर बैकट्रेस 47 | GoBack: ← पीछे 48 | NoScheduledFound: कोई परिगणित कार्य नही पाए गए 49 | When: कब 50 | ScheduledJobs: परिगणित कार्य 51 | idle: निष्क्रिय 52 | active: सक्रिय 53 | Version: वर्जन 54 | Connections: कनेक्श्न 55 | MemoryUsage: मेमरी उपयोग 56 | PeakMemoryUsage: अधिकतम मेमरी उपयोग 57 | Uptime: उपरिकाल (दिवस) 58 | OneWeek: १ सप्ताह 59 | OneMonth: १ महीना 60 | ThreeMonths: ३ महीने 61 | SixMonths: ६ महीने 62 | Failures: असफलता 63 | DeadJobs: निष्प्राण कार्य 64 | NoDeadJobsFound: कोई निष्प्राण कार्य नही पाए गए 65 | Dead: निष्प्राण 66 | Processes: प्रोसेसेस् 67 | Thread: थ्रेड 68 | Threads: थ्रेड्स् 69 | Jobs: कार्य 70 | Paused: थमे हुए 71 | Stop: रोको 72 | Quiet: शांत करो 73 | StopAll: सब रोको 74 | QuietAll: सब शांत करो 75 | PollingInterval: सर्वेक्षण अंतराल 76 | -------------------------------------------------------------------------------- /webui/static/locales/it.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | it: 3 | Dashboard: Dashboard 4 | Status: Stato 5 | Time: Ora 6 | Namespace: Namespace 7 | Realtime: Tempo reale 8 | History: Storia 9 | Busy: Occupato 10 | Processed: Processato 11 | Failed: Fallito 12 | Scheduled: Pianificato 13 | Retries: Nuovi tentativi 14 | Enqueued: In coda 15 | Worker: Lavoratore 16 | LivePoll: Live poll 17 | StopPolling: Ferma il polling 18 | Queue: Coda 19 | Class: Classe 20 | Job: Lavoro 21 | Arguments: Argomenti 22 | Extras: Extra 23 | Started: Iniziato 24 | ShowAll: Mostra tutti 25 | CurrentMessagesInQueue: Messaggi in %{queue} 26 | Delete: Cancella 27 | AddToQueue: Aggiungi alla coda 28 | AreYouSureDeleteJob: Sei sicuro di voler cancellare questo lavoro? 29 | AreYouSureDeleteQueue: Sei sicuro di voler cancellare la coda %{queue}? 30 | Queues: Code 31 | Size: Dimensione 32 | Actions: Azioni 33 | NextRetry: Prossimo tentativo 34 | RetryCount: Totale tentativi 35 | RetryNow: Riprova 36 | Kill: Uccidere 37 | LastRetry: Ultimo tentativo 38 | OriginallyFailed: Primo fallimento 39 | AreYouSure: Sei sicuro? 40 | DeleteAll: Cancella tutti 41 | RetryAll: Riprova tutti 42 | NoRetriesFound: Non sono stati trovati nuovi tentativi 43 | Error: Errore 44 | ErrorClass: Classe dell'errore 45 | ErrorMessage: Messaggio di errore 46 | ErrorBacktrace: Backtrace dell'errore 47 | GoBack: ← Indietro 48 | NoScheduledFound: Non ci sono lavori pianificati 49 | When: Quando 50 | ScheduledJobs: Lavori pianificati 51 | idle: inattivo 52 | active: attivo 53 | Version: Versione 54 | Connections: Connessioni 55 | MemoryUsage: Memoria utilizzata 56 | PeakMemoryUsage: Memoria utilizzata (max.) 57 | Uptime: Uptime (giorni) 58 | OneWeek: 1 settimana 59 | OneMonth: 1 mese 60 | ThreeMonths: 3 mesi 61 | SixMonths: 6 mesi 62 | Failures: Fallimenti 63 | DeadJobs: Lavori arrestati 64 | NoDeadJobsFound: Non ci sono lavori arrestati 65 | Dead: Arrestato 66 | Processes: Processi 67 | Thread: Thread 68 | Threads: Thread 69 | Jobs: Lavori 70 | -------------------------------------------------------------------------------- /webui/static/locales/ja.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | ja: 3 | Dashboard: ダッシュボード 4 | Status: 状態 5 | Time: 時間 6 | Namespace: ネームスペース 7 | Realtime: リアルタイム 8 | History: 履歴 9 | Busy: 実行中 10 | Processed: 完了 11 | Failed: 失敗 12 | Scheduled: 予定 13 | Retries: 再試行 14 | Enqueued: 待機状態 15 | Worker: 動作中の作業 16 | LivePoll: ポーリング開始 17 | StopPolling: ポーリング停止 18 | Queue: キュー 19 | Class: クラス 20 | Job: ジョブ 21 | Arguments: 引数 22 | Extras: エクストラ 23 | Started: 開始 24 | ShowAll: 全て見せる 25 | CurrentMessagesInQueue: %{queue}に メッセージがあります 26 | Delete: 削除 27 | ClearQueue: クリア 28 | AddToQueue: キューに追加 29 | AreYouSureDeleteJob: このジョブを削除しますか? 30 | AreYouSureDeleteQueue: この %{queue} キューを削除しますか? 31 | Queues: キュー 32 | Size: サイズ 33 | Actions: アクション 34 | NextRetry: 再試行 35 | RetryCount: 再試行 36 | RetryNow: 今すぐ再試行 37 | Kill: 強制終了 38 | LastRetry: 再試行履歴 39 | OriginallyFailed: 失敗 40 | AreYouSure: よろしいですか? 41 | DeleteAll: 全て削除 42 | RetryAll: 全て再試行 43 | NoRetriesFound: 再試行するジョブはありません 44 | Error: エラー 45 | ErrorClass: エラークラス 46 | ErrorMessage: エラーメッセージ 47 | ErrorBacktrace: エラーバックトレース 48 | GoBack: ← 戻る 49 | NoScheduledFound: 予定されたジョブはありません 50 | When: いつ 51 | ScheduledJobs: 予定されたジョブ 52 | idle: アイドル 53 | active: アクティブ 54 | Version: バージョン 55 | Connections: 接続 56 | MemoryUsage: メモリー使用量 57 | PeakMemoryUsage: 最大メモリー使用量 58 | Uptime: 連続稼働時間 (日) 59 | OneWeek: 1 週 60 | OneMonth: 1 ヶ月 61 | ThreeMonths: 3 ヶ月 62 | SixMonths: 6 ヶ月 63 | Failures: 失敗 64 | DeadJobs: デッドジョブ 65 | NoDeadJobsFound: デッドジョブはありません 66 | Dead: デッド 67 | Processes: プロセス 68 | Thread: スレッド 69 | Threads: スレッド 70 | Jobs: ジョブ 71 | Paused: 一時停止中 72 | Stop: 停止 73 | Quiet: 処理終了 74 | StopAll: すべて停止 75 | QuietAll: すべて処理終了 76 | PollingInterval: ポーリング間隔 77 | Plugins: プラグイン 78 | NotYetEnqueued: キューに入っていません 79 | CreatedAt: 作成日時 80 | BackToApp: アプリに戻る 81 | CommandsExecuted: 実行したコマンド 82 | -------------------------------------------------------------------------------- /webui/static/locales/ko.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | ko: 3 | Dashboard: 대시보드 4 | Status: 상태 5 | Time: 시간 6 | Namespace: 네임스페이스 7 | Realtime: 실시간 8 | History: 히스토리 9 | Busy: 작동 10 | Processed: 처리완료 11 | Failed: 실패 12 | Scheduled: 예약 13 | Retries: 재시도 14 | Enqueued: 대기 중 15 | Worker: 워커 16 | LivePoll: 폴링 시작 17 | StopPolling: 폴링 중단 18 | Queue: 큐 19 | Class: 클래스 20 | Job: 작업 21 | Arguments: 인자 22 | Started: 시작 23 | ShowAll: 모두 보기 24 | CurrentMessagesInQueue: %{queue}에 대기 중인 메시지 25 | Delete: 삭제 26 | AddToQueue: 큐 추가 27 | AreYouSureDeleteJob: 이 작업을 삭제하시겠습니까? 28 | AreYouSureDeleteQueue: 이 %{queue} 큐를 삭제하시겠습니까? 29 | Queues: 큐 30 | Size: 크기 31 | Actions: 동작 32 | NextRetry: 다음 재시도 33 | RetryCount: 재시도 횟수 34 | RetryNow: 지금 재시도 35 | LastRetry: 최근 재시도 36 | OriginallyFailed: 실패 37 | AreYouSure: 정말입니까? 38 | DeleteAll: 모두 삭제 39 | RetryAll: 모두 재시도 40 | NoRetriesFound: 재시도 내역이 없습니다 41 | Error: 에러 42 | ErrorClass: 에러 클래스 43 | ErrorMessage: 에러 메시지 44 | ErrorBacktrace: 에러 Backtrace 45 | GoBack: ← 뒤로 46 | NoScheduledFound: 예약된 작업이 없습니다 47 | When: 언제 48 | ScheduledJobs: 예약된 작업 49 | idle: 대기 중 50 | active: 동작 중 51 | Version: 버전 52 | Connections: 커넥션 53 | MemoryUsage: 메모리 사용량 54 | PeakMemoryUsage: 최대 메모리 사용량 55 | Uptime: 업타임 (일) 56 | OneWeek: 1 주 57 | OneMonth: 1 달 58 | ThreeMonths: 3 달 59 | SixMonths: 6 달 60 | Batches: 배치 61 | Failures: 실패 62 | DeadJobs: 죽은 작업 63 | NoDeadJobsFound: 죽은 작업이 없습니다 64 | Dead: 죽음 65 | Processes: 프로세스 66 | Thread: 스레드 67 | Threads: 스레드 68 | Jobs: 작업 69 | -------------------------------------------------------------------------------- /webui/static/locales/nb.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | nb: 3 | Dashboard: Oversikt 4 | Status: Status 5 | Time: Tid 6 | Namespace: Navnerom 7 | Realtime: Sanntid 8 | History: Historikk 9 | Busy: Opptatt 10 | Processed: Prosessert 11 | Failed: Mislykket 12 | Scheduled: Planlagt 13 | Retries: Forsøk 14 | Enqueued: I kø 15 | Worker: Arbeider 16 | LivePoll: Automatisk oppdatering 17 | StopPolling: Stopp automatisk oppdatering 18 | Queue: Kø 19 | Class: Klasse 20 | Job: Jobb 21 | Arguments: Argumenter 22 | Extras: Ekstra 23 | Started: Startet 24 | ShowAll: Vis alle 25 | CurrentMessagesInQueue: Nåværende melding i %{queue} 26 | Delete: Slett 27 | AddToQueue: Legg til i kø 28 | AreYouSureDeleteJob: Er du sikker på at du vil slette denne jobben? 29 | AreYouSureDeleteQueue: Er du sikker på at du vil slette køen %{queue}? 30 | Queues: Køer 31 | Size: Størrelse 32 | Actions: Handlinger 33 | NextRetry: Neste forsøk 34 | RetryCount: Antall forsøk 35 | RetryNow: Forsøk igjen nå 36 | Kill: Kill 37 | LastRetry: Forrige forsøk 38 | OriginallyFailed: Feilet opprinnelig 39 | AreYouSure: Er du sikker? 40 | DeleteAll: Slett alle 41 | RetryAll: Forsøk alle på nytt 42 | NoRetriesFound: Ingen forsøk funnet 43 | Error: Feil 44 | ErrorClass: Feilklasse 45 | ErrorMessage: Feilmelding 46 | ErrorBacktrace: Feilbakgrunn 47 | GoBack: ← Tilbake 48 | NoScheduledFound: Ingen planlagte jobber funnet 49 | When: Når 50 | ScheduledJobs: Planlagte jobber 51 | idle: uvirksom 52 | active: aktiv 53 | Version: Versjon 54 | Connections: Tilkoblinger 55 | MemoryUsage: Minneforbruk 56 | PeakMemoryUsage: Høyeste minneforbruk 57 | Uptime: Oppetid (dager) 58 | OneWeek: 1 uke 59 | OneMonth: 1 måned 60 | ThreeMonths: 3 måneder 61 | SixMonths: 6 måneder 62 | Failures: Feil 63 | DeadJobs: Døde jobber 64 | NoDeadJobsFound: Ingen døde jobber funnet 65 | Dead: Død 66 | Processes: Prosesser 67 | Thread: Tråd 68 | Threads: Tråder 69 | Jobs: Jobber 70 | Paused: Pauset 71 | Stop: Stopp 72 | Quiet: Demp 73 | StopAll: Stopp alle 74 | QuietAll: Demp alle 75 | PollingInterval: Oppdateringsintervall 76 | Plugins: Innstikk 77 | NotYetEnqueued: Ikke køet enda 78 | -------------------------------------------------------------------------------- /webui/static/locales/nl.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | nl: 3 | Dashboard: Dashboard 4 | Status: Status 5 | Time: Tijd 6 | Namespace: Namespace 7 | Realtime: Real-time 8 | History: Geschiedenis 9 | Busy: Bezet 10 | Processed: Verwerkt 11 | Failed: Mislukt 12 | Scheduled: Gepland 13 | Retries: Opnieuw proberen 14 | Enqueued: In de wachtrij 15 | Worker: Werker 16 | LivePoll: Live bijwerken 17 | StopPolling: Stop live bijwerken 18 | Queue: Wachtrij 19 | Class: Klasse 20 | Job: Taak 21 | Arguments: Argumenten 22 | Extras: Extra's 23 | Started: Gestart 24 | ShowAll: Toon alle 25 | CurrentMessagesInQueue: Aantal berichten in %{queue} 26 | Delete: Verwijderen 27 | AddToQueue: Toevoegen aan wachtrij 28 | AreYouSureDeleteJob: Weet u zeker dat u deze taak wilt verwijderen? 29 | AreYouSureDeleteQueue: Weet u zeker dat u wachtrij %{queue} wilt verwijderen? 30 | Queues: Wachtrijen 31 | Size: Grootte 32 | Actions: Acties 33 | NextRetry: Volgende opnieuw proberen 34 | RetryCount: Aantal opnieuw geprobeerd 35 | RetryNow: Nu opnieuw proberen 36 | LastRetry: Laatste poging 37 | OriginallyFailed: Oorspronkelijk mislukt 38 | AreYouSure: Weet u het zeker? 39 | DeleteAll: Alle verwijderen 40 | RetryAll: Alle opnieuw proberen 41 | NoRetriesFound: Geen opnieuw te proberen taken gevonden 42 | Error: Fout 43 | ErrorClass: Fout Klasse 44 | ErrorMessage: Foutmelding 45 | ErrorBacktrace: Fout Backtrace 46 | GoBack: ← Terug 47 | NoScheduledFound: Geen geplande taken gevonden 48 | When: Wanneer 49 | ScheduledJobs: Geplande taken 50 | idle: inactief 51 | active: actief 52 | Version: Versie 53 | Connections: Verbindingen 54 | MemoryUsage: Geheugengebruik 55 | PeakMemoryUsage: Piek geheugengebruik 56 | Uptime: Looptijd (dagen) 57 | OneWeek: 1 week 58 | OneMonth: 1 maand 59 | ThreeMonths: 3 maanden 60 | SixMonths: 6 maanden 61 | Failures: Mislukt 62 | DeadJobs: Overleden taken 63 | NoDeadJobsFound: Geen overleden taken gevonden 64 | Dead: Overleden 65 | Processes: Processen 66 | Thread: Thread 67 | Threads: Threads 68 | Jobs: Taken 69 | -------------------------------------------------------------------------------- /webui/static/locales/pl.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | pl: 3 | Dashboard: Kokpit 4 | Status: Status 5 | Time: Czas 6 | Namespace: Przestrzeń nazw 7 | Realtime: Czas rzeczywisty 8 | History: Historia 9 | Busy: Zajęte 10 | Processed: Ukończone 11 | Failed: Nieudane 12 | Scheduled: Zaplanowane 13 | Retries: Prób 14 | Enqueued: Zakolejkowane 15 | Worker: Worker 16 | LivePoll: Wczytuj na żywo 17 | StopPolling: Zatrzymaj wczytywanie na żywo 18 | Queue: Kolejka 19 | Class: Klasa 20 | Job: Zadanie 21 | Arguments: Argumenty 22 | Started: Rozpoczęte 23 | ShowAll: Pokaż wszystko 24 | CurrentMessagesInQueue: Aktualne wiadomości w kolejce %{queue} 25 | Delete: Usuń 26 | AddToQueue: dodaj do kolejki 27 | AreYouSureDeleteJob: Czy na pewno usunąć to zadanie? 28 | AreYouSureDeleteQueue: Czy na pewno usunąć kolejkę %{queue}? 29 | Queues: Kolejki 30 | Size: Rozmiar 31 | Actions: Akcje 32 | NextRetry: Kolejna próba 33 | RetryCount: Liczba prób 34 | RetryNow: Ponów teraz 35 | LastRetry: Ostatnie ponowienie 36 | OriginallyFailed: Ostatnio nieudane 37 | AreYouSure: Na pewno? 38 | DeleteAll: Usuń wszystko 39 | RetryAll: Powtórz wszystko 40 | NoRetriesFound: Brak powtórzeń 41 | Error: Błąd 42 | ErrorClass: Klasa błędu 43 | ErrorMessage: Wiadomosć błędu 44 | ErrorBacktrace: Wyjście błędu 45 | GoBack: ← Wróć 46 | NoScheduledFound: Brak zaplanowanych zadań 47 | When: Kiedy 48 | ScheduledJobs: Zaplanowane zadania 49 | idle: bezczynne 50 | active: aktywne 51 | Version: Wersja 52 | Connections: Połączenia 53 | MemoryUsage: Wykorzystanie pamięci 54 | PeakMemoryUsage: Największe wykorzystanie pamięci 55 | Uptime: Uptime (dni) 56 | OneWeek: 1 tydzień 57 | OneMonth: 1 miesiąc 58 | ThreeMonths: 3 miesiące 59 | SixMonths: 6 miesięcy 60 | -------------------------------------------------------------------------------- /webui/static/locales/pt-br.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | "pt-br": 3 | Dashboard: Painel 4 | Status: Status 5 | Time: Tempo 6 | Namespace: Namespace 7 | Realtime: Tempo real 8 | History: Histórico 9 | Busy: Ocupados 10 | Processed: Processados 11 | Failed: Falhas 12 | Scheduled: Agendados 13 | Retries: Tentativas 14 | Enqueued: Na fila 15 | Worker: Trabalhador 16 | LivePoll: Live Poll 17 | StopPolling: Parar Polling 18 | Queue: Fila 19 | Class: Classe 20 | Job: Tarefa 21 | Arguments: Argumentos 22 | Extras: Extras 23 | Started: Iniciados 24 | ShowAll: Mostrar todos 25 | CurrentMessagesInQueue: Mensagens atualmente na %{queue} 26 | Delete: Apagar 27 | AddToQueue: Adicionar à fila 28 | AreYouSureDeleteJob: Deseja deletar esta tarefa? 29 | AreYouSureDeleteQueue: Deseja deletar a %{queue} fila? 30 | Queues: Filas 31 | Size: Tamanho 32 | Actions: Ações 33 | NextRetry: Próxima Tentativa 34 | RetryCount: Número de Tentativas 35 | RetryNow: Tentar novamente agora 36 | LastRetry: Última tentativa 37 | OriginallyFailed: Falhou originalmente 38 | AreYouSure: Tem certeza? 39 | DeleteAll: Apagar tudo 40 | RetryAll: Tentar tudo novamente 41 | NoRetriesFound: Nenhuma tentativa encontrada 42 | Error: Erro 43 | ErrorClass: Classe de erro 44 | ErrorMessage: Mensagem de erro 45 | ErrorBacktrace: Rastreamento do erro 46 | GoBack: ← Voltar 47 | NoScheduledFound: Nenhuma tarefa agendada foi encontrada 48 | When: Quando 49 | ScheduledJobs: Tarefas agendadas 50 | idle: ocioso 51 | active: ativo 52 | Version: Versão 53 | Connections: Conexões 54 | MemoryUsage: Uso de memória 55 | PeakMemoryUsage: Pico de uso de memória 56 | Uptime: Dias rodando 57 | OneWeek: 1 semana 58 | OneMonth: 1 mês 59 | ThreeMonths: 3 meses 60 | SixMonths: 6 meses 61 | Failures : Falhas 62 | DeadJobs : Tarefas mortas 63 | NoDeadJobsFound : Nenhuma tarefa morta foi encontrada 64 | Dead : Morta 65 | Processes : Processos 66 | Thread : Thread 67 | Threads : Threads 68 | Jobs : Tarefas 69 | -------------------------------------------------------------------------------- /webui/static/locales/pt.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | pt: 3 | Dashboard: Dashboard 4 | Status: Estado 5 | Time: Tempo 6 | Namespace: Namespace 7 | Realtime: Tempo real 8 | History: Histórico 9 | Busy: Ocupado 10 | Processed: Processados 11 | Failed: Falhados 12 | Scheduled: Agendados 13 | Retries: Tentativas 14 | Enqueued: Em espera 15 | Worker: Worker 16 | LivePoll: Live Poll 17 | StopPolling: Desactivar Live Poll 18 | Queue: Fila 19 | Class: Classe 20 | Job: Tarefa 21 | Arguments: Argumentos 22 | Started: Iniciados 23 | ShowAll: Mostrar todos 24 | CurrentMessagesInQueue: Mensagens na fila %{queue} 25 | Delete: Apagar 26 | AddToQueue: Adicionar à fila 27 | AreYouSureDeleteJob: Tem a certeza que deseja eliminar esta tarefa? 28 | AreYouSureDeleteQueue: Tem a certeza que deseja eliminar a fila %{queue}? 29 | Queues: Filas 30 | Size: Tamanho 31 | Actions: Acções 32 | NextRetry: Próxima Tentativa 33 | RetryCount: Tentativas efectuadas 34 | RetryNow: Tentar novamente 35 | LastRetry: Última Tentativa 36 | OriginallyFailed: Falhou inicialmente 37 | AreYouSure: Tem a certeza? 38 | DeleteAll: Eliminar todos 39 | RetryAll: Tentar tudo novamente 40 | NoRetriesFound: Não foram encontradas tentativas 41 | Error: Erro 42 | ErrorClass: Classe de Erro 43 | ErrorMessage: Mensagem de erro 44 | ErrorBacktrace: Backtrace do Erro 45 | GoBack: ← Voltar 46 | NoScheduledFound: Não foram encontradas tarefas agendadas 47 | When: Quando 48 | ScheduledJobs: Tarefas agendadas 49 | idle: livre 50 | active: activo 51 | Version: Versão 52 | Connections: Conexões 53 | MemoryUsage: Utilização de Memória 54 | PeakMemoryUsage: Pico de utilização de memória 55 | Uptime: Uptime (em dias) 56 | OneWeek: 1 semana 57 | OneMonth: 1 mês 58 | ThreeMonths: 3 meses 59 | SixMonths: 6 meses 60 | Failures: Falhas 61 | DeadJobs: Tarefas mortas 62 | NoDeadJobsFound: Não foram encontradas tarefas mortas 63 | Dead: Morto 64 | Processes: Processos 65 | Thread: Thread 66 | Threads: Threads 67 | Jobs: Tarefas 68 | -------------------------------------------------------------------------------- /webui/static/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | Dashboard: Панель управления 3 | Status: Статус 4 | Time: Время 5 | Namespace: Пространство имен 6 | Realtime: Сейчас 7 | History: История 8 | Busy: Занят 9 | Processed: Обработано 10 | Failed: Провалено 11 | Scheduled: Запланировано 12 | Retries: Попытки 13 | Enqueued: В очереди 14 | Worker: Обработчик 15 | LivePoll: Постоянный опрос 16 | StopPolling: Остановить опрос 17 | Queue: Очередь 18 | Class: Класс 19 | Job: Задача 20 | Arguments: Аргументы 21 | Extras: Дополнительно 22 | Started: Запущено 23 | ShowAll: Показать все 24 | CurrentMessagesInQueue: Текущие задачи в очереди %{queue} 25 | Delete: Удалить 26 | AddToQueue: Добавить в очередь 27 | AreYouSureDeleteJob: Вы уверены, что хотите удалить эту задачу? 28 | AreYouSureDeleteQueue: Вы уверены, что хотите удалить очередь %{queue}? 29 | Queues: Очереди 30 | Size: Размер 31 | Actions: Действия 32 | NextRetry: Следующая попытка 33 | RetryCount: Кол-во попыток 34 | RetryNow: Повторить сейчас 35 | Kill: Убиваем 36 | LastRetry: Последняя попытка 37 | OriginallyFailed: Первый провал 38 | AreYouSure: Вы уверены? 39 | DeleteAll: Удалить все 40 | RetryAll: Повторить все 41 | NoRetriesFound: Нет попыток 42 | Error: Ошибка 43 | ErrorClass: Класс ошибки 44 | ErrorMessage: Сообщение об ошибке 45 | ErrorBacktrace: Трассировка ошибки 46 | GoBack: ← Назад 47 | NoScheduledFound: Нет запланированных задач 48 | When: Когда 49 | ScheduledJobs: Запланированные задачи 50 | idle: отдыхает 51 | active: активен 52 | Version: Версия 53 | Connections: Соединения 54 | MemoryUsage: Использование памяти 55 | PeakMemoryUsage: Максимальный расход памяти 56 | Uptime: Дня(ей) бесперебойной работы 57 | OneWeek: 1 неделя 58 | OneMonth: 1 месяц 59 | ThreeMonths: 3 месяца 60 | SixMonths: 6 месяцев 61 | Failures: Провалы 62 | DeadJobs: Убитые задачи 63 | NoDeadJobsFound: Нет убитых задач 64 | Dead: Убито 65 | Processes: Процессы 66 | Thread: Поток 67 | Threads: Потоки 68 | Jobs: Задачи 69 | Paused: Приостановлено 70 | Stop: Остановить 71 | Quiet: Отдыхать 72 | StopAll: Остановить все 73 | QuietAll: Отдыхать всем 74 | PollingInterval: Интервал опроса 75 | Plugins: Плагины 76 | NotYetEnqueued: Пока не в очереди 77 | CreatedAt: Создан 78 | BackToApp: Назад 79 | -------------------------------------------------------------------------------- /webui/static/locales/sv.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | sv: # <---- change this to your locale code 3 | Dashboard: Panel 4 | Status: Status 5 | Time: Tid 6 | Namespace: Namnrymd 7 | Realtime: Realtid 8 | History: Historik 9 | Busy: Upptagen 10 | Processed: Processerad 11 | Failed: Misslyckad 12 | Scheduled: Schemalagd 13 | Retries: Försök 14 | Enqueued: Köad 15 | Worker: Worker 16 | LivePoll: Live poll 17 | StopPolling: Stoppa polling 18 | Queue: Kö 19 | Class: Klass 20 | Job: Jobb 21 | Arguments: Argument 22 | Extras: Extra 23 | Started: Startad 24 | ShowAll: Visa alla 25 | CurrentMessagesInQueue: Jobb i %{queue} 26 | Delete: Ta bort 27 | AddToQueue: Lägg till i kö 28 | AreYouSureDeleteJob: Är du säker på att du vill ta bort detta jobb? 29 | AreYouSureDeleteQueue: Är du säker på att du vill ta bort kön %{queue}? 30 | Queues: Köer 31 | Size: Storlek 32 | Actions: Åtgärder 33 | NextRetry: Nästa försök 34 | RetryCount: Antal försök 35 | RetryNow: Försök nu 36 | LastRetry: Senaste försök 37 | OriginallyFailed: Misslyckades ursprungligen 38 | AreYouSure: Är du säker? 39 | DeleteAll: Ta bort alla 40 | RetryAll: Försök alla igen 41 | NoRetriesFound: Inga försök hittades 42 | Error: Fel 43 | ErrorClass: Felklass 44 | ErrorMessage: Felmeddelande 45 | ErrorBacktrace: Backtrace för fel 46 | GoBack: ← Bakåt 47 | NoScheduledFound: Inga schemalagda jobb hittades 48 | When: När 49 | ScheduledJobs: Schemalagda jobb 50 | idle: avvaktande 51 | active: aktiv 52 | Version: Version 53 | Connections: Anslutningar 54 | MemoryUsage: Minnesanvändning 55 | PeakMemoryUsage: Minnesanvändning (peak) 56 | Uptime: Upptid (dagar) 57 | OneWeek: 1 vecka 58 | OneMonth: 1 månad 59 | ThreeMonths: 3 månader 60 | SixMonths: 6 månader 61 | Failures: Failures 62 | DeadJobs: Döda jobb 63 | NoDeadJobsFound: Inga döda jobb hittades 64 | Dead: Död 65 | Processes: Processer 66 | Thread: Tråd 67 | Threads: Trådar 68 | Jobs: Jobb 69 | -------------------------------------------------------------------------------- /webui/static/locales/ta.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | ta: # <---- change this to your locale code 3 | Dashboard: டாஷ்போர்டு 4 | Status: நிலைமை 5 | Time: நேரம் 6 | Namespace: பெயர்வெளி 7 | Realtime: நேரலை 8 | History: வரலாறு 9 | Busy: பணிமிகுதி 10 | Processed: நிறையுற்றது 11 | Failed: தோல்வி 12 | Scheduled: திட்டமிடப்பட்ட 13 | Retries: மீண்டும் முயற்சிக்க, 14 | Enqueued: வரிசைப்படுத்தப்பட்டவை 15 | Worker: பணியாளர் 16 | LivePoll: நேரடி கணிப்பு 17 | StopPolling: நிறுத்து வாக்குப்பதிவு 18 | Queue: வரிசை 19 | Class: வகுப்பு 20 | Job: வேலை 21 | Arguments: வாதங்கள், 22 | Extras: உபரி 23 | Started: தொடங்குதல் 24 | ShowAll: அனைத்து காட்டு 25 | CurrentMessagesInQueue: தற்போதைய வேலைகள் %{queue} 26 | Delete: நீக்கு 27 | AddToQueue: வரிசையில் சேர் 28 | AreYouSureDeleteJob: நீ இந்த வேலையை நீக்க வேண்டும் என்று உறுதியாக இருக்கிறீர்களா? 29 | AreYouSureDeleteQueue: நீங்கள் %{queue} வரிசையில் நீக்க வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா? 30 | Queues: வரிசை 31 | Size: அளவு 32 | Actions: செயல்கள் 33 | NextRetry: அடுத்த, மீண்டும் முயற்சிக்கவும் 34 | RetryCount: கணிப்பீடு, மீண்டும் முயற்சிக்கவும் 35 | RetryNow: இப்போது மீண்டும் முயற்சி செய்க 36 | Kill: கொல் 37 | LastRetry: கடைசியாக, மீண்டும் முயற்சிக்கவும் 38 | OriginallyFailed: முதலில் தோல்வி 39 | AreYouSure: நீங்கள் உறுதியாக இருக்கிறீர்களா? 40 | DeleteAll: அனைத்து நீக்கு 41 | RetryAll: அனைத்து, மீண்டும் முயற்சிக்கவும் 42 | NoRetriesFound: இல்லை மீண்டும் காணப்படவில்லை 43 | Error: பிழை 44 | ErrorClass: பிழை வகுப்பு 45 | ErrorMessage: பிழை செய்தி 46 | ErrorBacktrace: பிழை பின்தேடுலை 47 | GoBack: பின்புறம் 48 | NoScheduledFound: திட்டமிட்ட வேலைகள் காணப்படவில்லை 49 | When: எப்பொழுது? 50 | ScheduledJobs: திட்டமிட்ட வேலைகள் 51 | idle: முடங்கு நேரம் 52 | active: செயலில் 53 | Version: பதிப்பு 54 | Connections: இணைப்புகள் 55 | MemoryUsage: நினைவக பயன்பாடு 56 | PeakMemoryUsage: உச்ச நினைவக பயன்பாடு 57 | Uptime: இயக்க நேரம் (நாட்கள்) 58 | OneWeek: 1 வாரம் 59 | OneMonth: 1 மாதம் 60 | ThreeMonths: 3 மாதங்கள் 61 | SixMonths: 6 மாதங்கள் 62 | Failures: தோல்விகள் 63 | DeadJobs: டெட் வேலைகள் 64 | NoDeadJobsFound: இறந்த வேலை எதுவும் இல்லை 65 | Dead: இறந்துபோன 66 | Processes: செயல்முறைகள் 67 | Thread: நூல் 68 | Threads: நூல்கள் 69 | Jobs: வேலை வாய்ப்புகள் 70 | Paused: தற்காலிக பணிநிறுத்தம் 71 | Stop: நிறுத்து 72 | Quiet: அமைதியான 73 | StopAll: நிறுத்து அனைத்து 74 | QuietAll: அமைதியான அனைத்து 75 | PollingInterval: வாக்குப்பதிவு இடைவெளி -------------------------------------------------------------------------------- /webui/static/locales/uk.yml: -------------------------------------------------------------------------------- 1 | uk: 2 | Dashboard: Панель керування 3 | Status: Статус 4 | Time: Час 5 | Namespace: Простір імен 6 | Realtime: Зараз 7 | History: Історія 8 | Busy: Зайнятих 9 | Processed: Опрацьовано 10 | Failed: Невдалих 11 | Scheduled: Заплановано 12 | Retries: Спроби 13 | Enqueued: У черзі 14 | Worker: Обробник 15 | LivePoll: Постійне опитування 16 | StopPolling: Зупинити опитування 17 | Queue: Черга 18 | Class: Клас 19 | Job: Задача 20 | Arguments: Аргументи 21 | Extras: Додатково 22 | Started: Запущено 23 | ShowAll: Відобразити усі 24 | CurrentMessagesInQueue: Поточні задачі у черзі %{queue} 25 | Delete: Видалити 26 | AddToQueue: Додати до черги 27 | AreYouSureDeleteJob: Ви впевнені у тому, що хочете видалити задачу? 28 | AreYouSureDeleteQueue: Ви впевнені у тому, що хочете видалити чергу %{queue}? 29 | Queues: Черги 30 | Size: Розмір 31 | Actions: Дії 32 | NextRetry: Наступна спроба 33 | RetryCount: Кількість спроб 34 | RetryNow: Повторити зараз 35 | Kill: Вбиваємо 36 | LastRetry: Остання спроба 37 | OriginallyFailed: Перша невдала спроба 38 | AreYouSure: Ви впевнені? 39 | DeleteAll: Видалити усі 40 | RetryAll: Повторити усі 41 | NoRetriesFound: Спроб не знайдено 42 | Error: Помилка 43 | ErrorClass: Клас помилки 44 | ErrorMessage: Повідомлення про помилку 45 | ErrorBacktrace: Трасування помилки 46 | GoBack: ← Назад 47 | NoScheduledFound: Запланованих задач не знайдено 48 | When: Коли 49 | ScheduledJobs: Заплановані задачі 50 | idle: незайнятий 51 | active: активний 52 | Version: Версія 53 | Connections: З'єднань 54 | MemoryUsage: Використання пам'яті 55 | PeakMemoryUsage: Максимальне використання пам'яті 56 | Uptime: Днів безперебійної роботи 57 | OneWeek: 1 тиждень 58 | OneMonth: 1 місяць 59 | ThreeMonths: 3 місяці 60 | SixMonths: 6 місяців 61 | Failures: Невдачі 62 | DeadJobs: Вбиті задачі 63 | NoDeadJobsFound: Вбитих задач не знайдено 64 | Dead: Вбитих 65 | Processes: Процеси 66 | Thread: Потік 67 | Threads: Потоки 68 | Jobs: Задачі 69 | Paused: Призупинено 70 | Stop: Зупинити 71 | Quiet: Призупинити 72 | StopAll: Зупинити усі 73 | QuietAll: Призупинити усі 74 | PollingInterval: Інтервал опитування 75 | Plugins: Плагіни 76 | NotYetEnqueued: Ще не в черзі 77 | -------------------------------------------------------------------------------- /webui/static/locales/ur.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | ur: 3 | TextDirection: rtl 4 | Dashboard: صفحۂ اول 5 | Status: اسٹیٹس 6 | Time: ﻭﻗﺖ 7 | Namespace: Namespace 8 | Realtime: ﺑﺮاﮦ ﺭاﺳﺖ 9 | History: ﺗﺎﺭﻳﺦ 10 | Busy: مصروف 11 | Processed: مکمل شدہ 12 | Failed: ﻧﺎکاﻡ ﺷﺪﮦ 13 | Scheduled: ﻁےﺷﺪﮦ 14 | Retries: ﺩﻭﺑﺎﺭﮦ کﻭﺷﻴﺶ 15 | Enqueued: قطار ميں شامل 16 | Worker: ورکر 17 | LivePoll: ﺑﺮاﮦ ﺭاﺳﺖ 18 | StopPolling: ﺑﺮاﮦ ﺭاﺳﺖ روکيے 19 | Queue: قطار 20 | Class: کلاس 21 | Job: جاب 22 | Arguments: دلائل 23 | Extras: اﺻﺎﻑی 24 | Started: شروع 25 | ShowAll: سارے دکھاو 26 | CurrentMessagesInQueue: قطار ميں موجود تمام پيغامات %{queue} 27 | AddToQueue: ﻗﻄﺎﺭ ميں شامل کريں 28 | AreYouSureDeleteJob: کيا آپ یقین جاب حتم کرنا چاھتے ہيں ؟ 29 | AreYouSureDeleteQueue: کيا آپ یقین قطار حتم کرنا چاھتے ہيں ؟ 30 | Delete: ﺣﺬﻑ 31 | Queues: قطاريں 32 | Size: ﺣﺠﻢ 33 | Actions: ﻋﻮاﻣﻞ 34 | NextRetry: اگلی کﻭﺷﻴﺶ 35 | RetryCount: دوبارہ کوشش کا مکمل شمار 36 | RetryNow: ابھی دوبارہ کوشش 37 | Kill: ختم کرديں 38 | LastRetry: گزشتہ کوشش 39 | OriginallyFailed: ابتادائ ناکامی 40 | AreYouSure: کيا یقین ؟ 41 | DeleteAll: ﺗﻤﺎﻡ ﺣﺬﻑ کر ديں 42 | RetryAll: ﺗﻤﺎﻡ کی ﺩﻭﺑﺎﺭﮦ کﻭﺷﻴﺶ کﺭيں 43 | NoRetriesFound: کویٔ ﺩﻭﺑﺎﺭﮦ کﻭﺷﻴﺶ نھيں ملی 44 | Error: مسئلہ 45 | ErrorClass: مسئلہ کی کلاس 46 | ErrorMessage: مسئلہ کی وجہ 47 | ErrorBacktrace: مسئلہ کی کی تحقیقات کريں 48 | GoBack: واپس جايں 49 | NoScheduledFound: کویٔ ﻁےﺷﺪﮦچيز نہیں ملی 50 | When: ﺏک 51 | ScheduledJobs: ﻁےﺷﺪﮦجاب 52 | idle: بیکار 53 | active: فعال 54 | Version: ورژن 55 | Connections: کنکشنز 56 | MemoryUsage: یاداشت کا استعمال 57 | PeakMemoryUsage: سب سے زيادہ یاداشت کا استعمال 58 | Uptime: اپ ٹائم 59 | OneWeek: ایک ہفتہ 60 | OneMonth: ایک مہینہ 61 | ThreeMonths: تین ماہ 62 | SixMonths: چھ ماہ 63 | Failures: ناکامیاں 64 | DeadJobs: ختم شدہ جاب 65 | NoDeadJobsFound: کویٔ ختم شدہ جاب نہيی ملی 66 | Dead: ختم شدہ 67 | Processes: ﻋﻤﻠﻴﺎﺕ 68 | Thread: موضوع 69 | Threads: موضوع 70 | Jobs: جابز 71 | Paused: موقوف 72 | Stop: بند کرو 73 | Quiet: ﺣﺘﻢ کﺭﻭ 74 | StopAll: ﺗﻤﺎﻡ ﺑﻨﺪ کﺭﻭ 75 | QuietAll: ﺗﻤﺎﻡ ﺣﺘﻢ کﺭﻭ 76 | PollingInterval: ﺑﺮاﮦ ﺭاﺳﺖ کا ﺩﻭﺭاﻧﻴﮧ 77 | Plugins: پلگ انز 78 | NotYetEnqueued: ﻗﺘﺎﺭميں شامل نھيں 79 | CreatedAt: ﺗﺎﺭﻳﺢ آﻏﺎﺯ 80 | BackToApp: ﻭاپﺱ صفحۂ اﻭﻝ پر 81 | -------------------------------------------------------------------------------- /webui/static/locales/vi.yml: -------------------------------------------------------------------------------- 1 | vi: # <---- change this to your locale code 2 | Dashboard: Bảng Điều Khiển 3 | Status: Trạng Thái 4 | Time: Thời Gian 5 | Namespace: Không gian 6 | Realtime: Thời gian thực 7 | History: Lịch sử 8 | Busy: Bận 9 | Processed: Đã xử lý 10 | Failed: Đã thất bại 11 | Scheduled: Đã lên lịch 12 | Retries: Thử lại 13 | Enqueued: Hàng đợi 14 | Worker: Worker 15 | LivePoll: Thăm dò trực tiếp 16 | StopPolling: Ngừng thăm dò 17 | Queue: Hàng đợi 18 | Class: Class 19 | Job: Tác vụ 20 | Arguments: Tham số 21 | Extras: Thêm 22 | Started: Đã bắt đầu 23 | ShowAll: Hiển thị tất cả 24 | CurrentMessagesInQueue: Số lượng công việc trong %{queue} 25 | Delete: Xoá 26 | ClearQueue: Làm sạch 27 | AddToQueue: Thêm vào hàng đợi 28 | AreYouSureDeleteJob: Bạn có chắc muốn xoá tác vụ này? 29 | AreYouSureDeleteQueue: Bạn có chắc muốn xoá %{queue} này? 30 | Queues: Các hàng đợi 31 | Size: Kích thước 32 | Actions: Các hành động 33 | NextRetry: Lần thử lại tiếp theo 34 | RetryCount: Số lần thử lại 35 | RetryNow: Thử lại ngay bây giờ 36 | Kill: Kill 37 | LastRetry: Lần thử cuối 38 | OriginallyFailed: Đã thất bại từ đầu 39 | AreYouSure: Bạn có chắc? 40 | DeleteAll: Xoá tất cả 41 | RetryAll: Thử lại tất cả 42 | NoRetriesFound: Không có lần thử lại nào được tìm thấy 43 | Error: Lỗi 44 | ErrorClass: Error Class 45 | ErrorMessage: Error Message 46 | ErrorBacktrace: Error Backtrace 47 | GoBack: ← Trở về 48 | NoScheduledFound: Không có tác vụ đã lên lịch nào được tìm thấy 49 | When: Khi 50 | ScheduledJobs: Những tác vụ đã được lên lịch 51 | idle: đang chờ 52 | active: đang hoạt động 53 | Version: Phiên bản 54 | Connections: Các kết nối 55 | MemoryUsage: Lượng bộ nhớ sử dụng 56 | PeakMemoryUsage: Lượng bộ nhớ sử dụng cao nhất 57 | Uptime: Thời gian hệ thống đã hoạt động (ngày) 58 | OneWeek: 1 tuần 59 | OneMonth: 1 tháng 60 | ThreeMonths: 3 tháng 61 | SixMonths: 6 tháng 62 | Failures: Các thất bại 63 | DeadJobs: Các tác vụ đã chết 64 | NoDeadJobsFound: Không có tác vụ chết nào được tìm thấy 65 | Dead: Chết 66 | Processes: Tiến trình xử lý 67 | Thread: Luồng xử lý 68 | Threads: Các luồng xử lý 69 | Jobs: Các tác vụ 70 | Paused: Tạm dừng 71 | Stop: Dừng 72 | Quiet: Im lặng 73 | StopAll: Dừng tất cả 74 | QuietAll: Im lặng tất cả 75 | PollingInterval: Khoảng thời gian thăm dò 76 | Plugins: Plugins 77 | NotYetEnqueued: Chưa vào được hàng đợi 78 | CreatedAt: Tạo lúc 79 | BackToApp: Trở về ứng dụng 80 | CommandsExecuted: Lệnh được thực thi 81 | 82 | -------------------------------------------------------------------------------- /webui/static/locales/zh-cn.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | zh-cn: # <---- change this to your locale code 3 | Dashboard: 信息板 4 | Status: 状态 5 | Time: 时间 6 | Namespace: 命名空间 7 | Realtime: 实时 8 | History: 历史记录 9 | Busy: 执行中 10 | Processed: 已处理 11 | Failed: 已失败 12 | Scheduled: 已计划 13 | Retries: 重试 14 | Enqueued: 已进入队列 15 | Worker: 工人 16 | LivePoll: 实时轮询 17 | StopPolling: 停止轮询 18 | Queue: 队列 19 | Class: 类别 20 | Job: 作业 21 | Arguments: 参数 22 | Extras: 额外的 23 | Started: 已开始 24 | ShowAll: 显示全部 25 | CurrentMessagesInQueue: 目前在%{queue}的作业 26 | Delete: 删除 27 | ClearQueue: 清空队列 28 | AddToQueue: 添加至队列 29 | AreYouSureDeleteJob: 你确定要删除这个作业么? 30 | AreYouSureDeleteQueue: 你确定要删除%{queue}这个队列? 31 | Queues: 队列 32 | Size: 容量 33 | Actions: 动作 34 | NextRetry: 下次重试 35 | RetryCount: 重试次數 36 | RetryNow: 现在重试 37 | Kill: 结束 38 | LastRetry: 最后一次重试 39 | OriginallyFailed: 原本已失败 40 | AreYouSure: 你确定? 41 | DeleteAll: 删除全部 42 | RetryAll: 重试全部 43 | NoRetriesFound: 沒有发现可重试 44 | Error: 错误 45 | ErrorClass: 错误类别 46 | ErrorMessage: 错误消息 47 | ErrorBacktrace: 错误的回调追踪 48 | GoBack: ← 返回 49 | NoScheduledFound: 沒有发现计划作业 50 | When: 当 51 | ScheduledJobs: 计划作业 52 | idle: 闲置 53 | active: 活动中 54 | Version: 版本 55 | Connections: 连接 56 | MemoryUsage: 内存占用 57 | PeakMemoryUsage: 内存占用峰值 58 | Uptime: 上线时间 (天数) 59 | OneWeek: 一周 60 | OneMonth: 一个月 61 | ThreeMonths: 三个月 62 | SixMonths: 六个月 63 | Failures: 失败 64 | DeadJobs: 已停滞作业 65 | NoDeadJobsFound: 沒有发现任何已停滞的作业 66 | Dead: 已停滞 67 | Processes: 处理中 68 | Thread: 线程 69 | Threads: 线程 70 | Jobs: 作业 71 | Paused: 已暂停 72 | Stop: 停止 73 | Quiet: 静默 74 | StopAll: 全部停止 75 | QuietAll: 全部静默 76 | PollingInterval: 拉取间隔 77 | Plugins: 插件 78 | NotYetEnqueued: 尚未入队 79 | CreatedAt: 创建时间 80 | BackToApp: 返回App 81 | CommandsExecuted: 已执行命令 82 | Pause: 暂停 83 | Resume: 继续 84 | RetriesRemaining: 剩余重试次数 85 | 86 | -------------------------------------------------------------------------------- /webui/static/locales/zh-tw.yml: -------------------------------------------------------------------------------- 1 | # elements like %{queue} are variables and should not be translated 2 | zh-tw: # <---- change this to your locale code 3 | Dashboard: 資訊主頁 4 | Status: 狀態 5 | Time: 時間 6 | Namespace: 命名空間 7 | Realtime: 即時 8 | History: 歷史資料 9 | Busy: 忙碌 10 | Processed: 已處理 11 | Failed: 已失敗 12 | Scheduled: 已排程 13 | Retries: 重試 14 | Enqueued: 已佇列 15 | Worker: 工人 16 | LivePoll: 即時輪詢 17 | StopPolling: 停止輪詢 18 | Queue: 佇列 19 | Class: 類別 20 | Job: 工作 21 | Arguments: 參數 22 | Extras: 額外的 23 | Started: 已開始 24 | ShowAll: 顯示全部 25 | CurrentMessagesInQueue: 目前在%{queue}的工作 26 | Delete: 刪除 27 | AddToQueue: 增加至佇列 28 | AreYouSureDeleteJob: 你確定要刪除這個工作嗎? 29 | AreYouSureDeleteQueue: 你確定要刪除%{queue}這個佇列? 30 | Queues: 佇列 31 | Size: 容量 32 | Actions: 動作 33 | NextRetry: 下次重試 34 | RetryCount: 重試次數 35 | RetryNow: 馬上重試 36 | LastRetry: 最後一次重試 37 | OriginallyFailed: 原本已失敗 38 | AreYouSure: 你確定? 39 | DeleteAll: 刪除全部 40 | RetryAll: 重試全部 41 | NoRetriesFound: 沒有發現可重試 42 | Error: 錯誤 43 | ErrorClass: 錯誤類別 44 | ErrorMessage: 錯誤訊息 45 | ErrorBacktrace: 錯誤的回調追踨 46 | GoBack: ← 返回 47 | NoScheduledFound: 沒有發現已排程的工作 48 | When: 當 49 | ScheduledJobs: 已排程的工作 50 | idle: 閒置 51 | active: 活動中 52 | Version: 版本 53 | Connections: 連線 54 | MemoryUsage: 記憶體使用量 55 | PeakMemoryUsage: 尖峰記憶體使用量 56 | Uptime: 上線時間 (天數) 57 | OneWeek: 一週 58 | OneMonth: 一個月 59 | ThreeMonths: 三個月 60 | SixMonths: 六個月 61 | Failures: 失敗 62 | DeadJobs: 停滯工作 63 | NoDeadJobsFound: 沒有發現任何停滯的工作 64 | Dead: 停滯 65 | Processes: 處理中 66 | Thread: 執行緒 67 | Threads: 執行緒 68 | Jobs: 工作 69 | -------------------------------------------------------------------------------- /webui/summary.ego: -------------------------------------------------------------------------------- 1 | <% 2 | package webui 3 | 4 | import "net/http" 5 | 6 | func ego_summary(w io.Writer, req *http.Request) { 7 | c := req.Context() 8 | store := ctx(req).Store() 9 | %> 10 | 50 | <% } %> 51 | -------------------------------------------------------------------------------- /webui/timeago.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2016 Justin Campbell 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | package webui 26 | 27 | import ( 28 | "fmt" 29 | "math" 30 | "time" 31 | ) 32 | 33 | const ( 34 | minute = 1 35 | hour = minute * 60 36 | day = hour * 24 37 | month = day * 30 38 | year = day * 365 39 | ) 40 | 41 | func Timeago(t time.Time) string { 42 | now := time.Now().UTC() 43 | 44 | var d time.Duration 45 | var suffix string 46 | 47 | if t.Before(now) { 48 | d = now.Sub(t) 49 | suffix = "ago" 50 | } else { 51 | d = t.Sub(now) 52 | suffix = "from now" 53 | } 54 | 55 | return fmt.Sprintf("%s %s", fromDuration(d), suffix) 56 | } 57 | 58 | func fromDuration(d time.Duration) string { 59 | seconds := round(d.Seconds()) 60 | 61 | if seconds < 30 { 62 | return "less than a minute" 63 | } 64 | 65 | if seconds < 90 { 66 | return "1 minute" 67 | } 68 | 69 | minutes := div(seconds, 60) 70 | 71 | if minutes < 45 { 72 | return fmt.Sprintf("%0d minutes", minutes) 73 | } 74 | 75 | hours := div(minutes, 60) 76 | 77 | if minutes < day { 78 | return fmt.Sprintf("about %s", pluralize(hours, "hour")) 79 | } 80 | 81 | if minutes < (42 * hour) { 82 | return "1 day" 83 | } 84 | 85 | days := div(hours, 24) 86 | 87 | if minutes < (30 * day) { 88 | return pluralize(days, "day") 89 | } 90 | 91 | months := div(days, 30) 92 | 93 | if minutes < (45 * day) { 94 | return "about 1 month" 95 | } 96 | 97 | if minutes < (60 * day) { 98 | return "about 2 months" 99 | } 100 | 101 | if minutes < year { 102 | return pluralize(months, "month") 103 | } 104 | 105 | rem := minutes % year 106 | years := minutes / year 107 | 108 | if rem < (3 * month) { 109 | return fmt.Sprintf("about %s", pluralize(years, "year")) 110 | } 111 | if rem < (9 * month) { 112 | return fmt.Sprintf("over %s", pluralize(years, "year")) 113 | } 114 | 115 | years++ 116 | return fmt.Sprintf("almost %s", pluralize(years, "year")) 117 | } 118 | 119 | func pluralize(i int, s string) string { 120 | var plural string 121 | if i != 1 { 122 | plural = "s" 123 | } 124 | return fmt.Sprintf("%d %s%s", i, s, plural) 125 | } 126 | 127 | func round(f float64) int { 128 | return int(math.Floor(f + 0.50)) 129 | } 130 | 131 | func div(numerator int, denominator int) int { 132 | rem := numerator % denominator 133 | result := numerator / denominator 134 | 135 | if rem >= (denominator / 2) { 136 | result++ 137 | } 138 | 139 | return result 140 | } 141 | --------------------------------------------------------------------------------