Initialize
Continous integration / Tests (push) Canceled after 0s
Testing clients / Build beanstalkd (push) Canceled after 0s
Continous integration / Docker build (push) Canceled after 0s
Testing clients / Test Pheanstalk PHP (push) Canceled after 0s
Testing clients / Test Greenstalk Python (3.9) (push) Canceled after 0s
Testing clients / Test Pystalk Python (3.10) (push) Canceled after 0s

This commit is contained in:
2026-10-15 10:52:58 +05:00
commit b9bcb01322
73 changed files with 12864 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
codecov:
notify:
require_ci_to_pass: yes
coverage:
precision: 2
round: down
range: "65...90"
status:
project:
default:
threshold: 0.5 # Allow the coverage to drop by threshold %, and posting a success status.
patch:
default:
target: 0% # trial operation
changes: no
parsers:
gcov:
branch_detection:
conditional: yes
loop: yes
method: no
macro: no
comment:
layout: "header, diff"
behavior: default
require_changes: no
ignore:
- ".git"
- "*.yml"
- "*.md"
# ignore test files
- "test*.c"
# ignore sd-daemon.* since it's vendored as external lib
- "sd-daemon.*"
# ignore folders and all its contents
- "adm/.*"
- "ct/.*"
- "doc/.*"
- "pkg/.*"
+6
View File
@@ -0,0 +1,6 @@
*.gcda
*.gcno
*.o
/vers.c
/beanstalkd
/News
+29
View File
@@ -0,0 +1,29 @@
---
name: Continous integration
on:
push:
branches:
- master
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Tests
run: make check
build:
name: Docker build
runs-on: ubuntu-latest
needs:
- tests
steps:
- uses: actions/checkout@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: docker build . -t `echo ghcr.io/${{ github.repository }}:latest | tr '[:upper:]' '[:lower:]'`
- run: docker push `echo ghcr.io/${{ github.repository }}:latest | tr '[:upper:]' '[:lower:]'`
+15
View File
@@ -0,0 +1,15 @@
---
name: Checks for PRs
on:
pull_request:
push:
branches-ignore:
- master
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Tests
run: make check
+109
View File
@@ -0,0 +1,109 @@
name: Testing clients
on:
- push
- pull_request
jobs:
build:
name: Build beanstalkd
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: make
- uses: actions/upload-artifact@v3
with:
name: beanstalkd
path: beanstalkd
pheanstalk:
name: Test Pheanstalk PHP
runs-on: ubuntu-latest
continue-on-error: true
needs:
- build
steps:
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
tools: phpunit
- uses: actions/download-artifact@v3
with:
name: beanstalkd
- name: Start beanstalkd
run: chmod +x ./beanstalkd && ./beanstalkd &
- uses: actions/checkout@v3
with:
repository: pheanstalk/pheanstalk
ref: v5
- name: Install dependencies including dev-dependencies
run: composer install
- name: Run tests
run: phpunit
env:
SERVER_HOST: localhost
greenstalk:
# Disable this test suite until https://github.com/justinmayhew/greenstalk/issues/9 is fixed
if: ${{ false }}
name: Test Greenstalk Python
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
python-version: [ 3.9 ]
needs:
- build
steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- uses: actions/checkout@v3
with:
repository: justinmayhew/greenstalk
- uses: actions/download-artifact@v3
id: download
with:
name: beanstalkd
- name: Make beanstalkd executable
run: chmod +x ${{steps.download.outputs.download-path}}/beanstalkd
- name: Run beanstalkd -v
run: ${{steps.download.outputs.download-path}}/beanstalkd -v
- name: Install dependencies
run: |
pip install pytest
pip install .
- name: Run tests
env:
BEANSTALKD_PATH: ${{steps.download.outputs.download-path}}/beanstalkd
run: make test
pystalk:
name: Test Pystalk Python
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
python-version: [ "3.10" ]
needs:
- build
steps:
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- uses: actions/checkout@v3
with:
repository: EasyPost/pystalk
- uses: actions/download-artifact@v3
id: download
with:
name: beanstalkd
- name: Make beanstalkd executable
run: chmod +x ${{steps.download.outputs.download-path}}/beanstalkd
- name: Run beanstalkd -v
run: ${{steps.download.outputs.download-path}}/beanstalkd -v
- name: Install dependencies
run: python -m pip install -r requirements-tests.txt -e .
- name: Run tests
env:
BEANSTALKD_PATH: ${{steps.download.outputs.download-path}}/beanstalkd
run: pytest tests/
+6
View File
@@ -0,0 +1,6 @@
*.gcda
*.gcno
*.o
/vers.c
/beanstalkd
/News
+86
View File
@@ -0,0 +1,86 @@
branches:
only:
- master
git:
quiet: true
depth: 5
language: c
addons:
apt:
packages:
- lcov
compiler:
- clang
- gcc
os:
- linux
- osx
env:
global:
- MAKEJOBS="-j$(getconf _NPROCESSORS_ONLN)"
- TRAVIS_COMMIT_LOG=`git log --format=fuller -2`
- COVERAGE=OFF # Currently works only with gcc on linux
before_script:
- '[[ "${TRAVIS_OS_NAME}-${TRAVIS_COMPILER}" != "linux-gcc" ]] || export COVERAGE="ON"'
script:
- |
if [ $COVERAGE = "ON" ]; then
export LDFLAGS=" -lgcov --coverage"
export CFLAGS="-O0 -ggdb -fprofile-arcs -ftest-coverage"
fi
- make $MAKEJOBS || ( echo "Build failure. Verbose build follows." && make V=1 ; false )
- make check -j1 VERBOSE=1
jobs:
include:
- stage: Benchmark
os: linux
compiler: gcc
before_script:
- export COVERAGE=OFF
script:
- make $MAKEJOBS
- make $MAKEJOBS bench
- stage: Benchmark
os: linux
compiler: gcc
env:
- CFLAGS="-march=native -mtune=native -O3"
before_script:
- export COVERAGE=OFF
script:
- make $MAKEJOBS
- make $MAKEJOBS bench
- stage: Benchmark
os: osx
compiler: clang
env:
- CFLAGS="-march=native -mtune=native -O3"
before_script:
- export COVERAGE=OFF
script:
- make $MAKEJOBS
- make $MAKEJOBS bench
- stage: Benchmark
os: osx
compiler: clang
before_script:
- export COVERAGE=OFF
script:
- make $MAKEJOBS
- make $MAKEJOBS bench
after_success:
- '[[ "$COVERAGE" != "ON" ]] || bash <(curl -s https://codecov.io/bash)'
after_script:
- printf "$TRAVIS_COMMIT_RANGE\n"
- printf "$TRAVIS_COMMIT_LOG\n"
+63
View File
@@ -0,0 +1,63 @@
# Критический Контекст
## Статус И Источники
- Здесь находится исходный Beanstalkd 1.13 на C99. Многопроцессная архитектура ниже принята как направление доработки, но ещё не реализована.
- Постановка: [epics/00-overview.md](epics/00-overview.md). Открытые решения перечислены в [epics/01-contract.md](epics/01-contract.md); не считать их уже утверждёнными.
- Исходные контракты: `README.md`, `doc/protocol.txt`, `doc/beanstalkd.ronn`, `doc/beanstalkd.1`. Каталог называется `doc`, не `docs`.
- `doc/beanstalkd.1` и HTML генерируются из `doc/beanstalkd.ronn`. При изменении CLI обновлять исходник руководства и производные документы штатным способом.
- Сохранять приоритеты проекта: стабильность, исправление ошибок, ясность, затем производительность; см. `CONTRIBUTING.md`.
## Принятое Решение
- Архитектура: процессы-владельцы групп tubes на одном хосте, frontend для TCP/протокола, IPC между frontend и владельцами, supervisor для управления процессами.
- Каждая tube имеет одного владельца; один владелец обслуживает несколько tubes. Все изменения её заданий и планирование выполняются последовательно в event loop владельца.
- У каждого owner slot отдельные состояние в памяти и WAL. Owner slot является стабильным логическим идентификатором, а не PID.
- На первом этапе consumers не резервируют задания одновременно из нескольких наблюдаемых tubes. Допускается временный watch list из нескольких tubes при настройке, например `watch target`, `ignore default`. На момент `reserve` / `reserve-with-timeout` наблюдаемая tube должна быть одна.
- Реакция на неподдерживаемый multi-watch reserve требует явного протокольного решения. Нельзя молча выбрать одну tube или выдать выдуманный стандартный ответ.
- Одна наблюдаемая tube не означает один job, одну резервацию или одного владельца на всё время TCP-сессии. Переключение watch list и `reserve-job` могут оставить у сессии резервации разных владельцев. Не вводить дополнительные ограничения без согласования.
- Одна горячая tube остаётся последовательной. Масштабируется совокупность независимых tubes, а не выполнение пользовательских задач внешними consumers.
## Владение Динамическими Tubes
- Постоянный дескриптор каждой tube не нужен. Объект появляется при `use`, `watch`, инициализации `default` или восстановлении заданий и удаляется после исчезновения удерживающих ссылок.
- Задание в любом живом состоянии, включая reserved, удерживает tube. Ссылки от удалённых сессий должны учитываться у владельца; авария frontend должна их освобождать.
- Владение определяется до создания объекта: `partition = stable_hash(tube_name) % PARTITION_COUNT`, затем `owner_slot = partition_map[partition]`.
- Алгоритм hash, параметры, число разделов и карта одинаковы у всех процессов и стабильны между перезапусками с данными. Их точный формат задаётся в эпиках 01 и 02.
- Конкурентный claim отдельной tube не нужен: все обращения с одним именем попадают одному владельцу; он локально выполняет find-or-create.
- Маршрутизация сама по себе ничего не создаёт. Например, `stats-tube` и `pause-tube` неизвестного имени должны сохранить `NOT_FOUND`.
- Жизненный цикл объекта tube не меняет принадлежность имени. Повторное создание использует того же владельца.
- Одновременно активен не более одного процесса на owner slot. При падении перезапускается тот же slot; нельзя направлять его имена к другому живому владельцу как обычный failover.
- Карта распределения фиксирована на первом этапе. `hash(name) % live_process_count`, автоматический rebalance и переназначение при сбое запрещены. Миграция требует отдельной процедуры.
## Инварианты Реализации
- Не передавать `Job*`, `Tube*`, `Conn*`, `File*` через IPC. Использовать сериализуемые ID, поколения процессов/сессий и явное владение данными.
- Сохранять последовательность команд и ответов на TCP-соединении, текущие проверки владения резервацией, priority/FIFO внутри tube, delay, TTR, timeout, `DEADLINE_SOON` и освобождение резерваций при disconnect.
- У задания не может быть двух действующих резерваций; один запрос reserve даёт не более одного результата. Повторное выполнение внешним consumer после TTR допустимо: exactly-once не обещается.
- ID должны быть уникальны в логическом сервере, маршрутизируемы и безопасны при частичном перезапуске. `job_pri_less()` использует ID для FIFO; новый формат ID не должен нарушать порядок внутри tube.
- IPC обязан иметь framing, лимиты памяти, backpressure, отмену ожиданий и защиту от устаревших сообщений. Не повторять автоматически `put` или другую мутацию с неизвестным результатом.
- Смерть владельца, frontend и supervisor является частью контракта, а не только эксплуатационной деталью. Недоступность владельца не равна `NOT_FOUND`.
- Не делить существующий WAL между независимыми writers и не снимать блокировку каталога ради многопроцессности. Изменение `Jobrec` требует следовать правилам `Walver` в `dat.h`.
- Не обещать более сильную durability, чем реализует выбранный режим `-b`, `-f`, `-F`. Успешный IPC-ответ сам по себе не означает fsync.
- Политика сохранения отдельного legacy-режима, включения новой архитектуры и переноса старого WAL согласуется в эпике 01 до изменения запуска и форматов. Не считать поддержку двух режимов уже утверждённым объёмом и не ломать существующее поведение/данные без явного решения.
## Карта Кода
| Область | Файлы и точки |
| --- | --- |
| Запуск и event loop | `main.c`, `serv.c:srvserve`, платформенные `linux.c`, `darwin.c`, `freebsd.c`, `sunos.c` |
| Протокол, scheduler, timers | `prot.c:dispatch_cmd`, `process_queue`, `prottick`, `conn_timeout` |
| Сессии и резервации | `conn.c:conn_reserve_job`, `connclose`, `dat.h:Conn` |
| ID и jobs | `job.c:next_id`, `all_jobs`, `job_pri_less`; `dat.h:Job`, `Jobrec` |
| Динамические tubes | `tube.c:tube_find_or_make`, `tube_iref`, `tube_dref` |
| WAL и восстановление | `walg.c`, `file.c`, `serv.c:srv_acquire_wal`, `prot.c:prot_replay` |
| Тесты | `testserv.c`, `testjobs.c`, `testheap.c`, `testms.c`, `testutil.c`, `ct/` |
## Проверки
- Сборка: `make`; регрессии: `make check`; существующие микробенчмарки: `make bench`.
- Команды требуют поддерживаемого Unix-окружения и toolchain. Нативный Windows не заявлен как поддерживаемая платформа; наличие WSL/контейнеров проверять, а не предполагать.
- `make bench` не заменяет end-to-end нагрузочный тест многопроцессного сервера.
- Для каждой реализации проверять один и несколько owner slots, ошибки IPC, disconnect, TTR, аварии и восстановление WAL. Критерии: `epics/07-validation.md`.
- Не выдавать статический анализ за измеренный выигрыш и не отмечать эпики завершёнными без проверок.
+35
View File
@@ -0,0 +1,35 @@
# Changelog
All notable changes to this project will be documented in this file.
## [Unreleased]
## [1.12] - 2020-06-04
- add support of UNIX domain sockets
- add support of Solaris/illumos
- add the "reserve-job" command
- add draining status to the "stats" command
- make fsync turned on by default when binlog is used: it's synced every 50ms instead of never
- replace vendored systemd files with libsystemd
- systemd usage can be controlled with USE_SYSTEMD=yes/no
- specify C99 as required compiler
## [1.11] - 2019-06-29
- add automated testing via TravisCI
- add System V init script
- enable code coverage
- misc. fixes and documentation improvements
## [1.10] - 2014-08-05
- fix crash on suspend or other EINTR (#220)
- document touch command’s TTR reset (#188)
- add some basic benchmark tests
- add DESTDIR support to Makefile
[unreleased]: https://github.com/beanstalkd/beanstalkd/compare/v1.12...HEAD
[1.12]: https://github.com/beanstalkd/beanstalkd/compare/v1.11...v1.12
[1.11]: https://github.com/beanstalkd/beanstalkd/compare/v1.10...v1.11
[1.10]: https://github.com/beanstalkd/beanstalkd/compare/v1.9...v1.10
+48
View File
@@ -0,0 +1,48 @@
# Contributing to beanstalkd
Greetings. Firstly, if you're thinking of contributing to
beanstalkd, thank you! It's the hard work of people like you that
keeps beanstalkd a high-quality codebase and running smoothly in
the demanding, high-volume production environment of the servers
of many organizations around the world.
Please note that this project is released with a Contributor
Code of Conduct. By participating in this project you agree
to abide by its terms. See CodeOfConduct.txt for details.
## General
This is a mature project, so it rarely takes on new features. We
mostly focus on stability, bug fixing, clarity, and performance,
in that order.
## Issues
When reporting a bug, please describe:
- which version of beanstalkd you're using
- steps to reproduce the bug
- the behavior you saw
- the behavior you expected
If you're not using the latest version, please consider also
testing with the latest. There's a good chance the bug you found
has already been fixed.
## Good commit messages
Please see how to write good commit messages in the Go contributing guide
[here](https://golang.org/doc/contribute.html#commit_messages).
## Pull Requests
When opening a pull request, try to keep the changes focused on
one topic and avoid unrelated changes (even small things, like
editing punctuation or whitespace in comments).
If you're making big changes, consider discussing it on the
mailing list first. You might save yourself a lot of time if it
turns out that the changes you want to make aren't a good fit for
the project. This is especially true if you are adding new
functionality.
+78
View File
@@ -0,0 +1,78 @@
Contributor Covenant Code of Conduct
Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others’ private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at beanstalk-team@googlegroups.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project’s leadership.
Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+13
View File
@@ -0,0 +1,13 @@
ARG BASE=alpine
FROM alpine as builder
RUN apk add --no-cache build-base git
COPY . /tmp/beanstalkd
RUN cd /tmp/beanstalkd && make
################################
ARG BASE
FROM ${BASE}
COPY --from=builder /tmp/beanstalkd/beanstalkd /usr/bin/
EXPOSE 11300
ENTRYPOINT ["/usr/bin/beanstalkd"]
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2007 The authors of beanstalkd.
Copyright in contributions to beanstalkd is retained
by the original copyright holder of each contribution.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+122
View File
@@ -0,0 +1,122 @@
PREFIX?=/usr/local
BINDIR=$(DESTDIR)$(PREFIX)/bin
override CFLAGS+=-Wall -Werror -Wformat=2 -g
override LDFLAGS?=
LDLIBS?=
OS?=$(shell uname | tr 'A-Z' 'a-z')
INSTALL?=install
PKG_CONFIG?=pkg-config
ifeq ($(OS),sunos)
override LDFLAGS += -lxnet -lsocket -lnsl
endif
VERS=$(shell ./vers.sh)
TARG=beanstalkd
MOFILE=main.o
OFILES=\
$(OS).o\
conn.o\
file.o\
heap.o\
job.o\
ms.o\
net.o\
primes.o\
prot.o\
serv.o\
time.o\
tube.o\
util.o\
vers.o\
walg.o\
TOFILES=\
testheap.o\
testjobs.o\
testms.o\
testserv.o\
testutil.o\
HFILES=\
dat.h\
ifeq ($(OS),linux)
LDLIBS+=-lrt
endif
# systemd support can be configured via USE_SYSTEMD:
# no: disabled
# yes: enabled, build fails if libsystemd is not found
# otherwise: enabled if libsystemd is found
ifneq ($(USE_SYSTEMD),no)
ifeq ($(shell $(PKG_CONFIG) --exists libsystemd && echo $$?),0)
LDLIBS+=$(shell $(PKG_CONFIG) --libs libsystemd)
CPPFLAGS+=-DHAVE_LIBSYSTEMD
else
ifeq ($(USE_SYSTEMD),yes)
$(error USE_SYSTEMD is set to "$(USE_SYSTEMD)", but $(PKG_CONFIG) cannot find libsystemd)
endif
endif
endif
CLEANFILES=\
vers.c\
$(wildcard *.gc*)
.PHONY: all
all: $(TARG)
$(TARG): $(OFILES) $(MOFILE)
$(LINK.o) -o $@ $^ $(LDLIBS)
.PHONY: install
install: $(BINDIR)/$(TARG)
$(BINDIR)/%: %
$(INSTALL) -d $(dir $@)
$(INSTALL) $< $@
CLEANFILES+=$(TARG)
$(OFILES) $(MOFILE): $(HFILES)
CLEANFILES+=$(wildcard *.o)
.PHONY: clean
clean:
rm -f $(CLEANFILES)
.PHONY: check
check: ct/_ctcheck
ct/_ctcheck
.PHONY: bench
bench: ct/_ctcheck
ct/_ctcheck -b
ct/_ctcheck: ct/_ctcheck.o ct/ct.o $(OFILES) $(TOFILES)
ct/_ctcheck.c: $(TOFILES) ct/gen
ct/gen $(TOFILES) >$@.part
mv $@.part $@
ct/ct.o ct/_ctcheck.o: ct/ct.h ct/internal.h
$(TOFILES): $(HFILES) ct/ct.h
CLEANFILES+=$(wildcard ct/_* ct/*.o ct/*.gc*)
ifneq ($(shell ./verc.sh),$(shell cat vers.c 2>/dev/null))
.PHONY: vers.c
endif
vers.c:
./verc.sh >vers.c
doc/beanstalkd.1 doc/beanstalkd.1.html: doc/beanstalkd.ronn
ronn $<
freebsd.o: darwin.c
+54
View File
@@ -0,0 +1,54 @@
[![Build Status](https://github.com/beanstalkd/beanstalkd/actions/workflows/build-latest.yaml/badge.svg)](https://github.com/beanstalkd/beanstalkd/actions/workflows/build-latest.yaml)
[![codecov](https://codecov.io/gh/beanstalkd/beanstalkd/branch/master/graph/badge.svg)](https://codecov.io/gh/beanstalkd/beanstalkd)
# beanstalkd
Simple and fast general purpose work queue.
https://beanstalkd.github.io/
See [doc/protocol.txt](https://github.com/beanstalkd/beanstalkd/blob/master/doc/protocol.txt)
for details of the network protocol.
Please note that this project is released with a Contributor
Code of Conduct. By participating in this project you agree
to abide by its terms. See CodeOfConduct.txt for details.
## Quick Start
$ make
$ ./beanstalkd
also try,
$ ./beanstalkd -h
$ ./beanstalkd -VVV
$ make CFLAGS=-O2
$ make CC=clang
$ make check
$ make install
$ make install PREFIX=/usr
Requires Linux (2.6.17 or later), Mac OS X, FreeBSD, or Illumos.
Currently beanstalkd is tested with GCC and clang, but it should work
with any compiler that supports C99.
Uses ronn to generate the manual.
See http://github.com/rtomayko/ronn.
## Subdirectories
- `adm` - files useful for system administrators
- `ct` - testing tool; vendored from https://github.com/kr/ct
- `doc` - documentation
- `pkg` - scripts to make releases
## Tests
Unit tests are in test*.c. See https://github.com/kr/ct for
information on how to write them.
+37
View File
@@ -0,0 +1,37 @@
The usual way to run beanstalkd is to type its name
in a Unix shell prompt, like this:
$ beanstalkd
This will start up the process and give you control over it.
You can control its output (by default output is printed to
the screen; you can arrange to have output go into file
b.log by typing ">b.log" at the end of the command line),
pause and restart the process (by pressing Control-Z and
typing "fg"), and kill it (by pressing Control-C).
This is most convenient while writing programs that use
beanstalkd (or when working on beanstalkd itself), since
you might want to start and stop it many times and regularly
inspect its output.
If you want beanstalkd to start when your operating system
boots, the mechanism varies.
Traditionally, you must add a command line to the shell script
in /etc/rc (which is read by init when the system boots),
using the "&" notation to run beanstalkd in the background.
This would suffice for most situations, but it isn't always
possible.
These days, many popular operating systems have a replacement
init program with its own configuration language. Example
configuration files for several of these are included in
subdirectories here, but the most common is probably "System V
init", which reads /etc/inittab for lines describing commands
to run at various times. If this file exists, you can add
a line something like
bean:345:respawn:su nobody -c 'exec /usr/bin/beanstalkd'
and type "telinit q" to tell init to reread its configuration.
Type "man 5 inittab" for details of this notation.
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>beanstalkd</string>
<key>UserName</key>
<string>nobody</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/beanstalkd</string>
</array>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=Beanstalkd is a simple, fast work queue
[Service]
User=nobody
ExecStart=/usr/bin/beanstalkd
[Install]
WantedBy=multi-user.target
+8
View File
@@ -0,0 +1,8 @@
[Unit]
Description=Socket for beanstalkd, a simple, fast work queue
[Socket]
ListenStream=11300
[Install]
WantedBy=sockets.target
+137
View File
@@ -0,0 +1,137 @@
#!/bin/sh
#
# System V init script in charge of starting/stopping beanstalkd
#
# chkconfig: - 57 47
# description: beanstalkd is a simple, fast work queue
# processname: beanstalkd
# config: /etc/sysconfig/beanstalkd
# pidfile: /var/run/beanstalkd/beanstalkd.pid
### BEGIN INIT INFO
# Provides: beanstalkd
# Required-Start: $local_fs $network $remote_fs
# Required-Stop: $local_fs $network $remote_fs
# Default-Stop: 0 1 2 6
# Short-Description: start and stop beanstalkd
# Description: beanstalkd is a simple, fast work queue
### END INIT INFO
# Source function library
. /etc/rc.d/init.d/functions
# Source networking configuration
. /etc/sysconfig/network
# Check that networking is up
[ "$NETWORKING" = "no" ] && exit
exec="/usr/bin/beanstalkd"
prog=$(basename $exec)
# Default options, overruled by items in sysconfig
BEANSTALKD_ADDR=127.0.0.1
BEANSTALKD_PORT=11300
BEANSTALKD_USER=beanstalkd
[ -e /etc/sysconfig/$prog ] && . /etc/sysconfig/$prog
lockfile=/var/lock/subsys/$prog
start() {
[ -x $exec ] || exit 5
echo -n $"Starting $prog: "
options="-l $BEANSTALKD_ADDR -p $BEANSTALKD_PORT -u $BEANSTALKD_USER"
[ -n "$BEANSTALKD_MAX_JOB_SIZE" ] && options="$options -z $BEANSTALKD_MAX_JOB_SIZE"
if [ -n "$BEANSTALKD_BINLOG_DIR" ]; then
if [ ! -d "$BEANSTALKD_BINLOG_DIR" ]; then
echo "Creating binlog directory ($BEANSTALKD_BINLOG_DIR)"
mkdir -p $BEANSTALKD_BINLOG_DIR
chown $BEANSTALKD_USER:$BEANSTALKD_USER $BEANSTALKD_BINLOG_DIR
fi
options="$options -b $BEANSTALKD_BINLOG_DIR"
if [ -n "$BEANSTALKD_BINLOG_FSYNC_PERIOD" ]; then
options="$options -f $BEANSTALKD_BINLOG_FSYNC_PERIOD"
else
options="$options -F"
fi
if [ -n "$BEANSTALKD_BINLOG_SIZE" ]; then
options="$options -s $BEANSTALKD_BINLOG_SIZE"
fi
fi
daemon "nohup $exec $options > /dev/null 2>&1 &"
retval=$?
echo
[ $retval -eq 0 ] && touch $lockfile
return $retval
}
stop() {
echo -n $"Stopping $prog: "
killproc $prog
retval=$?
echo
[ $retval -eq 0 ] && rm -f $lockfile
return $retval
}
restart() {
stop
start
}
reload() {
restart
}
force_reload() {
restart
}
rh_status() {
# Run checks to determine if the service is running or use generic status
status $prog
}
rh_status_q() {
rh_status >/dev/null 2>&1
}
case "$1" in
start)
rh_status_q && exit 0
$1
;;
stop)
rh_status_q || exit 0
$1
;;
restart)
$1
;;
reload)
rh_status_q || exit 7
$1
;;
force-reload)
force_reload
;;
status)
rh_status
;;
condrestart|try-restart)
rh_status_q || exit 0
restart
;;
*)
echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload}"
exit 2
esac
exit $?
+11
View File
@@ -0,0 +1,11 @@
description "simple, fast work queue"
start on filesystem
stop on runlevel [!2345]
respawn
respawn limit 5 2
setuid nobody
exec /usr/bin/beanstalkd
+88
View File
@@ -0,0 +1,88 @@
%define beanstalkd_user beanstalkd
%define beanstalkd_group %{beanstalkd_user}
%define beanstalkd_home %{_localstatedir}/lib/beanstalkd
%define beanstalkd_logdir %{_localstatedir}/log/beanstalkd
Name: beanstalkd
Version: 1.13
Release: 0%{?dist}
Summary: A simple, fast workqueue service
Group: System Environment/Daemons
License: GPLv3+
URL: http://xph.us/software/%{name}/
Source0: http://xph.us/dist/%{name}/rel/%{name}-%{version}.tar.gz
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
Requires(pre): %{_sbindir}/useradd
Requires(post): /sbin/chkconfig
Requires(preun): /sbin/chkconfig, /sbin/service
Requires(postun): /sbin/service
%description
Beanstalk is a simple, fast workqueue service. Its interface is generic, but
was originally designed for reducing the latency of page views in high-volume
web applications by running time-consuming tasks asynchronously.
%prep
%setup -q
if [ ! -e configure ]; then
sh buildconf.sh
fi
%build
%configure --disable-rpath --docdir=%{_defaultdocdir}/%{name}-%{version}
make %{?_smp_mflags}
%install
rm -rf $RPM_BUILD_ROOT
make install DESTDIR=$RPM_BUILD_ROOT
%{__install} -p -D -m 0644 doc/%{name}.1 %{buildroot}%{_mandir}/man1/%{name}.1
%{__install} -p -D -m 0755 scripts/%{name}.init %{buildroot}%{_initrddir}/%{name}
%{__install} -p -D -m 0644 scripts/%{name}.sysconfig %{buildroot}%{_sysconfdir}/sysconfig/%{name}
%clean
rm -rf $RPM_BUILD_ROOT
%pre
%{_sbindir}/useradd -c "beanstalkd user" -s /bin/false -r -m -d %{beanstalkd_home} %{beanstalkd_user} 2>/dev/null || :
%post
/sbin/chkconfig --add %{name}
%preun
if [ $1 = 0 ]; then
/sbin/service %{name} stop >/dev/null 2>&1
/sbin/chkconfig --del %{name}
fi
%postun
if [ $1 -ge 1 ]; then
/sbin/service %{name} condrestart > /dev/null 2>&1 || :
fi
%files
%defattr(-,root,root,-)
%doc %{_defaultdocdir}/%{name}-%{version}/protocol.txt
%doc README COPYING doc/protocol.txt
%{_initrddir}/%{name}
%{_bindir}/%{name}
%{_mandir}/man1/%{name}.1.gz
%config(noreplace) %{_sysconfdir}/sysconfig/%{name}
%changelog
* Thu Oct 1 2009 Keith Rarick <kr@xph.us> - 1.4-0
- Convert this file to an autoconf template
- Tweak the summary and description
* Sun Jan 4 2009 Ask Bjørn Hansen <ask@develooper.com> - 1.2-0
- 1.2-tobe
- Use man page and .init/sysconfig scripts from .tar.gz
* Sat Nov 22 2008 Jeremy Hinegardner <jeremy at hinegardner dot org> - 1.1-1
- initial spec creation
+273
View File
@@ -0,0 +1,273 @@
#include "dat.h"
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SAFETY_MARGIN (1000000000) /* 1 second */
static int cur_conn_ct = 0, cur_worker_ct = 0, cur_producer_ct = 0;
static uint tot_conn_ct = 0;
int verbose = 0;
static void
on_watch(Ms *a, Tube *t, size_t i)
{
UNUSED_PARAMETER(a);
UNUSED_PARAMETER(i);
tube_iref(t);
t->watching_ct++;
}
static void
on_ignore(Ms *a, Tube *t, size_t i)
{
UNUSED_PARAMETER(a);
UNUSED_PARAMETER(i);
t->watching_ct--;
tube_dref(t);
}
Conn *
make_conn(int fd, char start_state, Tube *use, Tube *watch)
{
Conn *c = new(Conn);
if (!c) {
twarn("OOM");
return NULL;
}
ms_init(&c->watch, (ms_event_fn) on_watch, (ms_event_fn) on_ignore);
if (!ms_append(&c->watch, watch)) {
free(c);
twarn("OOM");
return NULL;
}
TUBE_ASSIGN(c->use, use);
use->using_ct++;
c->state = start_state;
c->pending_timeout = -1;
c->tickpos = 0; // Does not mean anything if in_conns is set to 0.
c->in_conns = 0;
// The list is empty.
job_list_reset(&c->reserved_jobs);
/* stats */
cur_conn_ct++;
tot_conn_ct++;
return c;
}
void
connsetproducer(Conn *c)
{
if (c->type & CONN_TYPE_PRODUCER) return;
c->type |= CONN_TYPE_PRODUCER;
cur_producer_ct++; /* stats */
}
void
connsetworker(Conn *c)
{
if (c->type & CONN_TYPE_WORKER) return;
c->type |= CONN_TYPE_WORKER;
cur_worker_ct++; /* stats */
}
int
count_cur_conns()
{
return cur_conn_ct;
}
uint
count_tot_conns()
{
return tot_conn_ct;
}
int
count_cur_producers()
{
return cur_producer_ct;
}
int
count_cur_workers()
{
return cur_worker_ct;
}
static int
has_reserved_job(Conn *c)
{
return !job_list_is_empty(&c->reserved_jobs);
}
// Returns positive nanoseconds when c should tick, 0 otherwise.
static int64
conntickat(Conn *c)
{
int margin = 0, should_timeout = 0;
int64 t = INT64_MAX;
if (conn_waiting(c)) {
margin = SAFETY_MARGIN;
}
if (has_reserved_job(c)) {
t = connsoonestjob(c)->r.deadline_at - nanoseconds() - margin;
should_timeout = 1;
}
if (c->pending_timeout >= 0) {
t = min(t, ((int64)c->pending_timeout) * 1000000000);
should_timeout = 1;
}
if (should_timeout) {
return nanoseconds() + t;
}
return 0;
}
// Remove c from the c->srv heap and reschedule it using the value
// returned by conntickat if there is an outstanding timeout in the c.
void
connsched(Conn *c)
{
if (c->in_conns) {
heapremove(&c->srv->conns, c->tickpos);
c->in_conns = 0;
}
c->tickat = conntickat(c);
if (c->tickat) {
heapinsert(&c->srv->conns, c);
c->in_conns = 1;
}
}
// conn_set_soonestjob updates c->soonest_job with j
// if j should be handled sooner than c->soonest_job.
static void
conn_set_soonestjob(Conn *c, Job *j) {
if (!c->soonest_job || j->r.deadline_at < c->soonest_job->r.deadline_at) {
c->soonest_job = j;
}
}
// Return the reserved job with the earliest deadline,
// or NULL if there's no reserved job.
Job *
connsoonestjob(Conn *c)
{
// use cached value and bail out.
if (c->soonest_job != NULL)
return c->soonest_job;
Job *j = NULL;
for (j = c->reserved_jobs.next; j != &c->reserved_jobs; j = j->next) {
conn_set_soonestjob(c, j);
}
return c->soonest_job;
}
void
conn_reserve_job(Conn *c, Job *j) {
j->tube->stat.reserved_ct++;
j->r.reserve_ct++;
j->r.deadline_at = nanoseconds() + j->r.ttr;
j->r.state = Reserved;
job_list_insert(&c->reserved_jobs, j);
j->reserver = c;
c->pending_timeout = -1;
conn_set_soonestjob(c, j);
}
// Return true if c has a reserved job with less than one second until its
// deadline.
int
conndeadlinesoon(Conn *c)
{
int64 t = nanoseconds();
Job *j = connsoonestjob(c);
return j && t >= j->r.deadline_at - SAFETY_MARGIN;
}
int
conn_ready(Conn *c)
{
size_t i;
for (i = 0; i < c->watch.len; i++) {
if (((Tube *) c->watch.items[i])->ready.len)
return 1;
}
return 0;
}
int
conn_less(void *ca, void *cb)
{
Conn *a = (Conn *)ca;
Conn *b = (Conn *)cb;
return a->tickat < b->tickat;
}
void
conn_setpos(void *c, size_t i)
{
((Conn *)c)->tickpos = i;
}
void
connclose(Conn *c)
{
sockwant(&c->sock, 0);
close(c->sock.fd);
if (verbose) {
printf("close %d\n", c->sock.fd);
}
job_free(c->in_job);
/* was this a peek or stats command? */
if (c->out_job && c->out_job->r.state == Copy)
job_free(c->out_job);
c->in_job = c->out_job = NULL;
c->in_job_read = 0;
if (c->type & CONN_TYPE_PRODUCER) cur_producer_ct--; /* stats */
if (c->type & CONN_TYPE_WORKER) cur_worker_ct--; /* stats */
cur_conn_ct--; /* stats */
remove_waiting_conn(c);
if (has_reserved_job(c))
enqueue_reserved_jobs(c);
ms_clear(&c->watch);
c->use->using_ct--;
TUBE_ASSIGN(c->use, NULL);
if (c->in_conns) {
heapremove(&c->srv->conns, c->tickpos);
c->in_conns = 0;
}
free(c);
}
+2
View File
@@ -0,0 +1,2 @@
/*.o
/_*
+19
View File
@@ -0,0 +1,19 @@
Copyright © 2010–2013 Keith Rarick
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+574
View File
@@ -0,0 +1,574 @@
/* CT - simple-minded unit testing for C */
#include <signal.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <sys/time.h>
#include <stdint.h>
#include <inttypes.h>
#include "internal.h"
#include "ct.h"
static char *curdir;
static int rjobfd = -1, wjobfd = -1;
int fail = 0; /* bool */
static int64 bstart, bdur;
static int btiming; /* bool */
static int64 bbytes;
enum { Second = 1000 * 1000 * 1000 };
enum { BenchTime = Second };
enum { MaxN = 1000 * 1000 * 1000 };
#ifdef __MACH__
# include <mach/mach_time.h>
static int64
nstime()
{
return (int64)mach_absolute_time();
}
#else
# include <time.h>
static int64
nstime()
{
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (int64)(t.tv_sec)*Second + t.tv_nsec;
}
#endif
void
ctlogpn(const char *p, int n, const char *fmt, ...)
{
va_list arg;
printf("%s:%d: ", p, n);
va_start(arg, fmt);
vprintf(fmt, arg);
va_end(arg);
putchar('\n');
}
void
ctfail(void)
{
fail = 1;
}
void
ctfailnow(void)
{
fflush(NULL);
abort();
}
char *
ctdir(void)
{
return curdir;
}
void
ctresettimer(void)
{
bdur = 0;
bstart = nstime();
}
void
ctstarttimer(void)
{
if (!btiming) {
bstart = nstime();
btiming = 1;
}
}
void
ctstoptimer(void)
{
if (btiming) {
bdur += nstime() - bstart;
btiming = 0;
}
}
void
ctsetbytes(int n)
{
bbytes = (int64)n;
}
static void
die(int code, int err, const char *msg)
{
putc('\n', stderr);
if (msg && *msg) {
fputs(msg, stderr);
fputs(": ", stderr);
}
fputs(strerror(err), stderr);
putc('\n', stderr);
exit(code);
}
static int
tmpfd(void)
{
FILE *f = tmpfile();
if (!f) {
die(1, errno, "tmpfile");
}
return fileno(f);
}
static int
failed(int s)
{
return WIFSIGNALED(s) && (WTERMSIG(s) == SIGABRT);
}
static void
waittest(Test *ts)
{
Test *t;
int pid, stat;
pid = wait3(&stat, 0, 0);
if (pid == -1) {
die(3, errno, "wait");
}
killpg(pid, SIGKILL);
for (t=ts; t->f; t++) {
if (t->pid == pid) {
t->status = stat;
if (!t->status) {
putchar('.');
} else if (failed(t->status)) {
putchar('F');
} else {
putchar('E');
}
fflush(stdout);
}
}
}
static void
start(Test *t)
{
t->fd = tmpfd();
strcpy(t->dir, TmpDirPat);
if (mkdtemp(t->dir) == NULL) {
die(1, errno, "mkdtemp");
}
fflush(NULL);
t->pid = fork();
if (t->pid < 0) {
die(1, errno, "fork");
} else if (!t->pid) {
setpgid(0, 0);
if (dup2(t->fd, 1) == -1) {
die(3, errno, "dup2");
}
if (close(t->fd) == -1) {
die(3, errno, "fclose");
}
if (dup2(1, 2) == -1) {
die(3, errno, "dup2");
}
curdir = t->dir;
t->f();
if (fail) {
ctfailnow();
}
exit(0);
}
setpgid(t->pid, t->pid);
}
static void
runalltest(Test *ts, int limit)
{
int nrun = 0;
Test *t;
for (t=ts; t->f; t++) {
if (nrun >= limit) {
waittest(ts);
nrun--;
}
start(t);
nrun++;
}
for (; nrun; nrun--) {
waittest(ts);
}
}
static void
copyfd(FILE *out, int in)
{
ssize_t n;
char buf[1024]; /* arbitrary size */
while ((n = read(in, buf, sizeof(buf))) != 0) {
if (fwrite(buf, 1, n, out) != (size_t)n) {
die(3, errno, "fwrite");
}
}
}
/*
Removes path and all of its children.
Writes errors to stderr and keeps going.
If path doesn't exist, rmtree returns silently.
*/
static void
rmtree(char *path)
{
int r = unlink(path);
if (r == 0 || errno == ENOENT) {
return; /* success */
}
int unlinkerr = errno;
DIR *d = opendir(path);
if (!d) {
if (errno == ENOTDIR) {
fprintf(stderr, "ct: unlink: %s\n", strerror(unlinkerr));
} else {
perror("ct: opendir");
}
fprintf(stderr, "ct: path %s\n", path);
return;
}
struct dirent *ent;
while ((ent = readdir(d))) {
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
}
int n = strlen(path) + 1 + strlen(ent->d_name);
char s[n+1];
sprintf(s, "%s/%s", path, ent->d_name);
rmtree(s);
}
closedir(d);
r = rmdir(path);
if (r == -1) {
perror("ct: rmdir");
fprintf(stderr, "ct: path %s\n", path);
}
}
static void
runbenchn(Benchmark *b, int n)
{
int outfd = tmpfd();
int durfd = tmpfd();
strcpy(b->dir, TmpDirPat);
if (mkdtemp(b->dir) == NULL) {
die(1, errno, "mkdtemp");
}
fflush(NULL);
int pid = fork();
if (pid < 0) {
die(1, errno, "fork");
} else if (!pid) {
setpgid(0, 0);
if (dup2(outfd, 1) == -1) {
die(3, errno, "dup2");
}
if (close(outfd) == -1) {
die(3, errno, "fclose");
}
if (dup2(1, 2) == -1) {
die(3, errno, "dup2");
}
curdir = b->dir;
ctstarttimer();
b->f(n);
ctstoptimer();
if (write(durfd, &bdur, sizeof bdur) != sizeof bdur) {
die(3, errno, "write");
}
if (write(durfd, &bbytes, sizeof bbytes) != sizeof bbytes) {
die(3, errno, "write");
}
exit(0);
}
setpgid(pid, pid);
pid = waitpid(pid, &b->status, 0);
if (pid == -1) {
die(3, errno, "wait");
}
killpg(pid, SIGKILL);
rmtree(b->dir);
if (b->status != 0) {
putchar('\n');
lseek(outfd, 0, SEEK_SET);
copyfd(stdout, outfd);
return;
}
lseek(durfd, 0, SEEK_SET);
int r = read(durfd, &b->dur, sizeof b->dur);
if (r != sizeof b->dur) {
perror("read");
b->status = 1;
}
r = read(durfd, &b->bytes, sizeof b->bytes);
if (r != sizeof b->bytes) {
perror("read");
b->status = 1;
}
}
/* rounddown10 rounds a number down to the nearest power of 10. */
static int
rounddown10(int n)
{
int tens = 0;
/* tens = floor(log_10(n)) */
while (n >= 10) {
n = n / 10;
tens++;
}
/* result = 10**tens */
int i, result = 1;
for (i = 0; i < tens; i++) {
result *= 10;
}
return result;
}
/* roundup rounds n up to a number of the form [1eX, 2eX, 5eX]. */
static int
roundup(int n)
{
int base = rounddown10(n);
if (n <= base)
return base;
if (n <= 2*base)
return 2*base;
if (n <= 3*base)
return 3*base;
if (n <= 5*base)
return 5*base;
return 10*base;
}
static int
min(int a, int b)
{
if (a < b) {
return a;
}
return b;
}
static int
max(int a, int b)
{
if (a > b) {
return a;
}
return b;
}
static void
runbench(Benchmark *b)
{
printf("%s\t", b->name);
fflush(stdout);
int n = 1;
runbenchn(b, n);
while (b->status == 0 && b->dur < BenchTime && n < MaxN) {
int last = n;
/* Predict iterations/sec. */
int nsop = b->dur / n;
if (nsop == 0) {
n = MaxN;
} else {
n = BenchTime / nsop;
}
/* Run more iterations than we think we'll need for a second (1.2x).
Don't grow too fast in case we had timing errors previously.
Be sure to run at least one more than last time. */
n = max(min(n+n/5, 100*last), last+1);
/* Round up to something easy to read. */
n = roundup(n);
runbenchn(b, n);
}
if (b->status == 0) {
printf("%8d\t%10" PRId64 " ns/op", n, b->dur/n);
if (b->bytes > 0) {
double mbs = 0;
if (b->dur > 0) {
int64 sec = b->dur / 1000L / 1000L / 1000L;
int64 nsec = b->dur % 1000000000L;
double dur = (double)sec + (double)nsec*.0000000001;
mbs = ((double)b->bytes * (double)n / 1000000) / dur;
}
printf("\t%7.2f MB/s", mbs);
}
putchar('\n');
} else {
if (failed(b->status)) {
printf("failure");
} else {
printf("error");
if (WIFEXITED(b->status)) {
printf(" (exit status %d)", WEXITSTATUS(b->status));
}
if (WIFSIGNALED(b->status)) {
printf(" (signal %d)", WTERMSIG(b->status));
}
}
putchar('\n');
}
}
static void
runallbench(Benchmark *b)
{
for (; b->f; b++) {
runbench(b);
}
}
static int
report(Test *t)
{
int nfail = 0, nerr = 0;
putchar('\n');
for (; t->f; t++) {
rmtree(t->dir);
if (!t->status) {
continue;
}
printf("\n%s: ", t->name);
if (failed(t->status)) {
nfail++;
printf("failure");
} else {
nerr++;
printf("error");
if (WIFEXITED(t->status)) {
printf(" (exit status %d)", WEXITSTATUS(t->status));
}
if (WIFSIGNALED(t->status)) {
printf(" (signal %d)", WTERMSIG(t->status));
}
}
putchar('\n');
lseek(t->fd, 0, SEEK_SET);
copyfd(stdout, t->fd);
}
if (nfail || nerr) {
printf("\n%d failures; %d errors.\n", nfail, nerr);
} else {
printf("\nPASS\n");
}
return nfail || nerr;
}
static int
readtokens()
{
int n = 1;
char c, *s;
char *v = getenv("MAKEFLAGS");
if (v == NULL)
return n;
if ((s = strstr(v, " --jobserver-fds="))) {
rjobfd = (int)strtol(s+17, &s, 10); /* skip " --jobserver-fds=" */
wjobfd = (int)strtol(s+1, NULL, 10); /* skip comma */
}
if (rjobfd >= 0) {
fcntl(rjobfd, F_SETFL, fcntl(rjobfd, F_GETFL)|O_NONBLOCK);
while (read(rjobfd, &c, 1) > 0) {
n++;
}
}
return n;
}
static void
writetokens(int n)
{
char c = '+';
if (wjobfd >= 0) {
fcntl(wjobfd, F_SETFL, fcntl(wjobfd, F_GETFL)|O_NONBLOCK);
for (; n>1; n--) {
if (write(wjobfd, &c, 1) != 1) {
/* ignore error; nothing we can do anyway */
}
}
}
}
int
main(int argc, char **argv)
{
int n = readtokens();
runalltest(ctmaintest, n);
writetokens(n);
int code = report(ctmaintest);
if (code != 0) {
return code;
}
if (argc == 2 && strcmp(argv[1], "-b") == 0) {
runallbench(ctmainbench);
}
return 0;
}
+18
View File
@@ -0,0 +1,18 @@
char *ctdir(void);
void ctfail(void);
void ctfailnow(void);
void ctresettimer(void);
void ctstarttimer(void);
void ctstoptimer(void);
void ctsetbytes(int);
void ctlogpn(const char*, int, const char*, ...) __attribute__((format(printf, 3, 4)));
#define ctlog(...) ctlogpn(__FILE__, __LINE__, __VA_ARGS__)
#define assert(x) do if (!(x)) {\
ctlog("%s", "test: " #x);\
ctfailnow();\
} while (0)
#define assertf(x, ...) do if (!(x)) {\
ctlog("%s", "test: " #x);\
ctlog(__VA_ARGS__);\
ctfailnow();\
} while (0)
+46
View File
@@ -0,0 +1,46 @@
#!/bin/sh
set -e
fixsyms() {
if test "`uname -s|tr A-Z a-z`" = darwin
then egrep -v [.] | egrep ^_ | sed s/^_//
else cat
fi
}
syms() {
prefix=$1
shift
for f in "$@"
do nm $f
done | cut -d ' ' -f 3 | fixsyms | egrep ^$prefix
}
ts=`syms cttest "$@" || true`
bs=`syms ctbench "$@" || true`
printf '#include <stdint.h>\n'
printf '#include "internal.h"\n'
for t in $ts
do printf 'void %s(void);\n' $t
done
for b in $bs
do printf 'void %s(int);\n' $b
done
printf 'Test ctmaintest[] = {\n'
for t in $ts
do printf ' {%s, "%s", 0, 0, 0, TmpDirPat},\n' $t $t
done
printf ' {.f = 0},\n'
printf '};\n'
printf 'Benchmark ctmainbench[] = {\n'
for b in $bs
do printf ' {%s, "%s", 0, 0, 0, TmpDirPat},\n' $b $b
done
printf ' {.f = 0},\n'
printf '};\n'
+28
View File
@@ -0,0 +1,28 @@
/* include <stdint.h> */
#define TmpDirPat "/tmp/ct.XXXXXX"
typedef int64_t int64;
typedef struct Test Test;
typedef struct Benchmark Benchmark;
struct Test {
void (*f)(void);
const char *name;
int status;
int fd;
int pid;
char dir[sizeof TmpDirPat];
};
struct Benchmark {
void (*f)(int);
const char *name;
int status;
int64 dur;
int64 bytes;
char dir[sizeof TmpDirPat];
};
extern Test ctmaintest[];
extern Benchmark ctmainbench[];
+101
View File
@@ -0,0 +1,101 @@
#include "dat.h"
#include <stdint.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/event.h>
#include <sys/time.h>
enum
{
Infinity = 1 << 30
};
static int kq;
int
sockinit(void)
{
kq = kqueue();
if (kq == -1) {
twarn("kqueue");
return -1;
}
return 0;
}
int
sockwant(Socket *s, int rw)
{
int n = 0;
struct kevent evs[2] = {{0}};
struct kevent *ev = evs;
struct timespec ts = {.tv_sec = 0, .tv_nsec = 0};
if (s->added) {
ev->ident = s->fd;
ev->filter = s->added;
ev->flags = EV_DELETE;
ev++;
n++;
}
if (rw) {
ev->ident = s->fd;
switch (rw) {
case 'r':
ev->filter = EVFILT_READ;
break;
case 'w':
ev->filter = EVFILT_WRITE;
break;
default:
// check only for hangup
ev->filter = EVFILT_READ;
ev->fflags = NOTE_LOWAT;
ev->data = Infinity;
}
ev->flags = EV_ADD;
ev->udata = s;
s->added = ev->filter;
ev++;
n++;
}
return kevent(kq, evs, n, NULL, 0, &ts);
}
int
socknext(Socket **s, int64 timeout)
{
int r;
struct kevent ev;
static struct timespec ts;
ts.tv_sec = timeout / 1000000000;
ts.tv_nsec = timeout % 1000000000;
r = kevent(kq, NULL, 0, &ev, 1, &ts);
if (r == -1 && errno != EINTR) {
twarn("kevent");
return -1;
}
if (r > 0) {
*s = ev.udata;
if (ev.flags & EV_EOF) {
return 'h';
}
switch (ev.filter) {
case EVFILT_READ:
return 'r';
case EVFILT_WRITE:
return 'w';
}
}
return 0;
}
+497
View File
@@ -0,0 +1,497 @@
#include <stdint.h>
#include <stdlib.h>
typedef unsigned char uchar;
typedef uchar byte;
typedef unsigned int uint;
typedef int32_t int32;
typedef uint32_t uint32;
typedef int64_t int64;
typedef uint64_t uint64;
typedef struct Ms Ms;
typedef struct Job Job;
typedef struct Tube Tube;
typedef struct Conn Conn;
typedef struct Heap Heap;
typedef struct Jobrec Jobrec;
typedef struct File File;
typedef struct Socket Socket;
typedef struct Server Server;
typedef struct Wal Wal;
typedef void(*Handle)(void*, int rw);
typedef int(FAlloc)(int, int);
// NUM_PRIMES is used in the jobs hashing.
#if _LP64
#define NUM_PRIMES 48
#else
#define NUM_PRIMES 19
#endif
/* Some compilers (e.g. gcc on SmartOS) define NULL as 0.
* This is allowed by the C standard, but is unhelpful when
* using NULL in most pointer contexts with errors turned on. */
#if (defined(sun) || defined(__sun)) && (defined(__SVR4) || defined(__svr4__))
#ifdef NULL
#undef NULL
#endif
#define NULL ((void*)0)
#endif
// The name of a tube cannot be longer than MAX_TUBE_NAME_LEN-1
#define MAX_TUBE_NAME_LEN 201
// A command can be at most LINE_BUF_SIZE chars, including "\r\n". This value
// MUST be enough to hold the longest possible command ("pause-tube a{200} 4294967295\r\n")
// or reply line ("USING a{200}\r\n").
#define LINE_BUF_SIZE (11 + MAX_TUBE_NAME_LEN + 12)
#define min(a,b) ((a)<(b)?(a):(b))
// Jobs with priority less than URGENT_THRESHOLD are counted as urgent.
#define URGENT_THRESHOLD 1024
// The default maximum job size.
#define JOB_DATA_SIZE_LIMIT_DEFAULT ((1 << 16) - 1)
// The maximum value that job_data_size_limit can be set to via "-z".
// It could be up to INT32_MAX-2 (~2GB), but set it to 1024^3 (1GB).
// The width is restricted by Jobrec.body_size that is int32.
#define JOB_DATA_SIZE_LIMIT_MAX 1073741824
// The default value for the fsync (-f) parameter, milliseconds.
#define DEFAULT_FSYNC_MS 50
// Use this macro to designate unused parameters in functions.
#define UNUSED_PARAMETER(x) (void)(x)
// version is defined in vers.c, see vers.sh for details.
extern const char version[];
// verbose holds the count of -V parameters; it's a verbosity level.
extern int verbose;
extern struct Server srv;
// Replaced by tests to simulate failures.
extern FAlloc *falloc;
// stats structure holds counters for operations, both globally and per tube.
struct stats {
uint64 urgent_ct;
uint64 waiting_ct;
uint64 buried_ct;
uint64 reserved_ct;
uint64 pause_ct;
uint64 total_delete_ct;
uint64 total_jobs_ct;
};
// less_fn is used by the binary heap to determine the order of elements.
typedef int(*less_fn)(void*, void*);
// setpos_fn is used by the binary heap to record the new positions of elements
// whenever they get moved or inserted.
typedef void(*setpos_fn)(void*, size_t);
struct Heap {
size_t cap; // capacity of the heap
size_t len; // amount of elements in the heap
void **data; // actual elements
less_fn less;
setpos_fn setpos;
};
int heapinsert(Heap *h, void *x);
void* heapremove(Heap *h, size_t k);
struct Socket {
// Descriptor for the socket.
int fd;
// f can point to srvaccept or prothandle.
Handle f;
// x is passed as first parameter to f.
void *x;
// added value is platform dependend: on OSX it can be > 1.
// Value of 1 - socket was already added to event notifications,
// otherwise it is 0.
int added;
};
int sockinit(void);
// sockwant updates event filter for the socket s. rw designates
// the kind of event we should be notified about:
// 'r' - read
// 'w' - write
// 'h' - hangup (closed connection)
// 0 - ignore this socket
int sockwant(Socket *s, int rw);
// socknext waits for the next event at most timeout nanoseconds.
// If event happens before timeout then s points to the corresponding socket,
// and the kind of event is returned. In case of timeout, 0 is returned.
int socknext(Socket **s, int64 timeout);
// ms_event_fn is called with the element being inserted/removed and its position.
typedef void(*ms_event_fn)(Ms *a, void *item, size_t i);
// Resizable multiset
struct Ms {
size_t len; // amount of stored elements
size_t cap; // capacity
size_t last; // position of last taken element
void **items;
ms_event_fn oninsert; // called on insertion of an element
ms_event_fn onremove; // called on removal of an element
};
void ms_init(Ms *a, ms_event_fn oninsert, ms_event_fn onremove);
void ms_clear(Ms *a);
int ms_append(Ms *a, void *item);
int ms_remove(Ms *a, void *item);
int ms_contains(Ms *a, void *item);
void *ms_take(Ms *a);
enum // Jobrec.state
{
Invalid,
Ready,
Reserved,
Buried,
Delayed,
Copy
};
enum
{
Walver = 7
};
// If you modify Jobrec struct, you must increment Walver above.
//
// This workflow is expected:
// 1. If any change needs to be made to the format, first increment Walver.
// 2. If and only if this is the first such change since the last release:
// a. Copy-paste relevant file-reading functions in file.c and
// add the old version number to their names. For example,
// if you are incrementing Walver from 7 to 8, copy readrec to readrec7.
// (Currently, there is only one such function, readrec. But if
// a future readrec calls other version-specific functions,
// those will have to be copied too.)
// 3. Add a switch case to fileread for the old version.
// 4. Modify the current reading function (readrec) to reflect your change.
//
// Incrementing Walver for every change, even if not every version
// will be released, is helpful even if it "wastes" version numbers.
// It is a really easy thing to do and it means during development
// you won't have to worry about misinterpreting the contents of a binlog
// that you generated with a dev copy of beanstalkd.
struct Jobrec {
uint64 id;
uint32 pri;
int64 delay;
int64 ttr;
int32 body_size;
int64 created_at;
// deadline_at is a timestamp, in nsec, that points to:
// * time when job will become ready for delayed job,
// * time when TTR is about to expire for reserved job,
// * undefined otherwise.
int64 deadline_at;
uint32 reserve_ct;
uint32 timeout_ct;
uint32 release_ct;
uint32 bury_ct;
uint32 kick_ct;
byte state;
};
struct Job {
// persistent fields; these get written to the wal
Jobrec r;
// bookeeping fields; these are in-memory only
char pad[6];
Tube *tube;
Job *prev, *next; // linked list of jobs
Job *ht_next; // Next job in a hash table list
size_t heap_index; // where is this job in its current heap
File *file;
Job *fnext;
Job *fprev;
void *reserver;
int walresv;
int walused;
char *body; // written separately to the wal
};
struct Tube {
uint refs;
char name[MAX_TUBE_NAME_LEN];
Heap ready;
Heap delay;
Ms waiting_conns; // conns waiting for the job at this moment
struct stats stat;
uint using_ct;
uint watching_ct;
// pause is set to the duration of the current pause, otherwise 0, in nsec.
int64 pause;
// unpause_at is a timestamp when to unpause the tube, in nsec.
int64 unpause_at;
Job buried; // linked list header
};
// Prints warning message on stderr in the format:
// <progname>: FILE:LINE in FUNC: <fmt>: <errno_msg>
#define twarn(...) __twarn(__VA_ARGS__, "")
// Hack to quiet the compiler. When VA_ARGS in twarn() has one element,
// e.g. twarn("OOM"), its replaced with __twarn("OOM", ""),
// thus VA_ARGS is expanded to at least one element in warn().
#define __twarn(fmt, ...) \
warn("%s:%d in %s: " fmt "%s", __FILE__, __LINE__, __func__, __VA_ARGS__)
// Prints warning message on stderr in the format:
// <progname>: FILE:LINE in FUNC: <fmt>
#define twarnx(...) __twarnx(__VA_ARGS__, "")
// See __twarn macro.
#define __twarnx(fmt, ...) \
warnx("%s:%d in %s: " fmt "%s", __FILE__, __LINE__, __func__, __VA_ARGS__)
void warn(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
void warnx(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
char* fmtalloc(char *fmt, ...) __attribute__((format(printf, 1, 2)));
void* zalloc(int n);
#define new(T) zalloc(sizeof(T))
void optparse(Server*, char**);
extern const char *progname;
int64 nanoseconds(void);
int rawfalloc(int fd, int len);
// Take ID for a jobs from next_id and allocate and store the job.
#define make_job(pri,delay,ttr,body_size,tube) \
make_job_with_id(pri,delay,ttr,body_size,tube,0)
Job *allocate_job(int body_size);
Job *make_job_with_id(uint pri, int64 delay, int64 ttr,
int body_size, Tube *tube, uint64 id);
void job_free(Job *j);
/* Lookup a job by job ID */
Job *job_find(uint64 job_id);
/* the void* parameters are really job pointers */
void job_setpos(void *j, size_t pos);
int job_pri_less(void *ja, void *jb);
int job_delay_less(void *ja, void *jb);
Job *job_copy(Job *j);
const char * job_state(Job *j);
void job_list_reset(Job *head);
int job_list_is_empty(Job *head);
Job *job_list_remove(Job *j);
void job_list_insert(Job *head, Job *j);
/* for unit tests */
size_t get_all_jobs_used(void);
extern struct Ms tubes;
Tube *make_tube(const char *name);
void tube_dref(Tube *t);
void tube_iref(Tube *t);
Tube *tube_find(Ms *tubeset, const char *name);
Tube *tube_find_or_make(const char *name);
#define TUBE_ASSIGN(a,b) (tube_dref(a), (a) = (b), tube_iref(a))
Conn *make_conn(int fd, char start_state, Tube *use, Tube *watch);
int count_cur_conns(void);
uint count_tot_conns(void);
int count_cur_producers(void);
int count_cur_workers(void);
extern size_t primes[];
extern size_t job_data_size_limit;
void prot_init(void);
int64 prottick(Server *s);
void remove_waiting_conn(Conn *c);
void enqueue_reserved_jobs(Conn *c);
void enter_drain_mode(int sig);
void h_accept(const int fd, const short which, Server *s);
int prot_replay(Server *s, Job *list);
int make_server_socket(char *host, char *port);
// CONN_TYPE_* are bit masks used to track the type of connection.
// A put command adds the PRODUCER type, "reserve*" adds the WORKER type.
// If connection awaits for data, then it has WAITING type.
#define CONN_TYPE_PRODUCER 1
#define CONN_TYPE_WORKER 2
#define CONN_TYPE_WAITING 4
struct Conn {
Server *srv;
Socket sock;
char state; // see the STATE_* description
char type; // combination of CONN_TYPE_* values
Conn *next; // only used in epollq functions
Tube *use; // tube currently in use
int64 tickat; // time at which to do more work; determines pos in heap
size_t tickpos; // position in srv->conns, stale when in_conns=0
byte in_conns; // 1 if the conn is in srv->conns heap, 0 otherwise
Job *soonest_job;// memoization of the soonest job
int rw; // currently want: 'r', 'w', or 'h'
// How long client should "wait" for the next job; -1 means forever.
int pending_timeout;
// Used to inform state machine that client no longer waits for the data.
char halfclosed;
char cmd[LINE_BUF_SIZE]; // this string is NOT NUL-terminated
size_t cmd_len;
int cmd_read;
char *reply;
int reply_len;
int reply_sent;
char reply_buf[LINE_BUF_SIZE]; // this string IS NUL-terminated
// How many bytes of in_job->body have been read so far. If in_job is NULL
// while in_job_read is nonzero, we are in bit bucket mode and
// in_job_read's meaning is inverted -- then it counts the bytes that
// remain to be thrown away.
int64 in_job_read;
Job *in_job; // a job to be read from the client
Job *out_job; // a job to be sent to the client
int out_job_sent; // how many bytes of *out_job were sent already
Ms watch; // the set of watched tubes by the connection
Job reserved_jobs; // linked list header
};
int conn_less(void *ca, void *cb);
void conn_setpos(void *c, size_t i);
void connsched(Conn *c);
void connclose(Conn *c);
void connsetproducer(Conn *c);
void connsetworker(Conn *c);
Job *connsoonestjob(Conn *c);
int conndeadlinesoon(Conn *c);
int conn_ready(Conn *c);
void conn_reserve_job(Conn *c, Job *j);
#define conn_waiting(c) ((c)->type & CONN_TYPE_WAITING)
enum
{
Filesizedef = (10 << 20)
};
struct Wal {
int filesize;
int use;
char *dir;
File *head;
File *cur;
File *tail;
int nfile;
int next;
int64 resv; // bytes reserved
int64 alive; // bytes in use
int64 nmig; // migrations
int64 nrec; // records written ever
int wantsync; // do we sync to disk?
int64 syncrate; // how often we sync to disk, in nanoseconds
int64 lastsync;
};
int waldirlock(Wal*);
void walinit(Wal*, Job *list);
int walwrite(Wal*, Job*);
void walmaint(Wal*);
int walresvput(Wal*, Job*);
int walresvupdate(Wal*);
void walgc(Wal*);
struct File {
File *next;
uint refs;
int seq;
int iswopen; // is open for writing
int fd;
int free;
int resv;
char *path;
Wal *w;
Job jlist; // jobs written in this file
};
int fileinit(File*, Wal*, int);
Wal* fileadd(File*, Wal*);
void fileincref(File*);
void filedecref(File*);
void fileaddjob(File*, Job*);
void filermjob(File*, Job*);
int fileread(File*, Job *list);
void filewopen(File*);
void filewclose(File*);
int filewrjobshort(File*, Job*);
int filewrjobfull(File*, Job*);
#define Portdef "11300"
struct Server {
char *port;
char *addr;
char *user;
Wal wal;
Socket sock;
// Connections that must produce deadline or timeout, ordered by the time.
Heap conns;
};
void srv_acquire_wal(Server *s);
void srvserve(Server *s);
void srvaccept(Server *s, int ev);
+79
View File
@@ -0,0 +1,79 @@
.\" generated with Ronn-NG/v0.8.2
.\" http://github.com/apjanke/ronn-ng/tree/0.8.2
.TH "BEANSTALKD" "1" "June 2020" "" ""
.SH "NAME"
\fBbeanstalkd\fR \- simple, fast work queue
.SH "SYNOPSIS"
\fBbeanstalkd\fR [\fIoptions\fR]
.SH "DESCRIPTION"
\fBBeanstalkd\fR is a simple work\-queue service\. Its interface is generic, though it was originally designed for reducing the latency of page views in high\-volume web applications by running time\-consuming tasks asynchronously\.
.P
When started, \fBbeanstalkd\fR opens a socket (or uses a file descriptor provided by the init(1) system, see \fI\%#ENVIRONMENT\fR) and listens for incoming connections\. For each connection, it reads a sequence of commands to create, reserve, delete, and otherwise manipulate "jobs", units of work to be done\. See file \fBdoc/protocol\.txt\fR in the \fBbeanstalkd\fR distribution for a thorough description of the meaning and format of the \fBbeanstalkd\fR protocol\.
.SH "OPTIONS"
.TP
\fB\-b\fR \fIpath\fR
Use a binlog to keep jobs on persistent storage in directory \fIpath\fR\. Upon startup, \fBbeanstalkd\fR will recover any binlog that is present in \fIpath\fR, then, during normal operation, append new jobs and changes in state to the binlog\.
.TP
\fB\-f\fR \fIms\fR
Call fsync(2) at most once every \fIms\fR milliseconds\. Larger values for \fIms\fR reduce disk activity and improve speed at the cost of safety\. A power failure could result in the loss of up to \fIms\fR milliseconds of history\.
.IP
A \fIms\fR value of 0 will cause \fBbeanstalkd\fR to call fsync every time it writes to the binlog\.
.IP
The default behavior is to sync every 50 ms\.
.IP
(This option has no effect without \fB\-b\fR\.)
.TP
\fB\-F\fR
Never call fsync(2)\. Equivalent to \fB\-f\fR with an infinite \fIms\fR value\.
.IP
(This option has no effect without \fB\-b\fR\.)
.TP
\fB\-h\fR
Show a brief help message and exit\.
.TP
\fB\-l\fR \fIaddr\fR
Listen on address \fIaddr\fR (default is 0\.0\.0\.0)\.
.IP
When \fIaddr\fR starts with "unix:", the unprefixed value of it will be used as the local filesystem path to create a UNIX socket instead of a TCP socket\. In this case the value of \fB\-p\fR will be ignored\.
.IP
(Option \fB\-l\fR has no effect if sd\-daemon(5) socket activation is being used\. See also \fI\%#ENVIRONMENT\fR\.)
.TP
\fB\-p\fR \fIport\fR
Listen on TCP port \fIport\fR (default is 11300)\.
.IP
(Option \fB\-p\fR has no effect if sd\-daemon(5) socket activation is being used\. See also \fI\%#ENVIRONMENT\fR\.)
.TP
\fB\-s\fR \fIbytes\fR
The size in bytes of each binlog file\.
.IP
(This option has no effect without \fB\-b\fR\.)
.TP
\fB\-u\fR \fIuser\fR
Become the user \fIuser\fR and its primary group\.
.TP
\fB\-V\fR
Increase verbosity\. May be used more than once to produce more verbose output\. The output format is subject to change\.
.TP
\fB\-v\fR
Print the version string and exit\.
.TP
\fB\-z\fR \fIbytes\fR
The maximum size in bytes of a job\.
.TP
\fB\-c\fR
This flag has no effect\. It is kept for historical compatibility only\.
.TP
\fB\-n\fR
This flag has no effect\. It is kept for historical compatibility only\.
.SH "ENVIRONMENT"
.TP
\fBLISTEN_PID\fR, \fBLISTEN_FDS\fR
These variables can be set by init(1)\. See sd_listen_fds(3) for details\.
.SH "SEE ALSO"
sd\-daemon(3), sd_listen_fds(3)
.P
Files \fBREADME\.md\fR and \fBdoc/protocol\.txt\fR in the \fBbeanstalkd\fR distribution\.
.P
\fI\%https://beanstalkd\.github\.io/\fR
.SH "AUTHOR"
\fBBeanstalkd\fR is written by Keith Rarick and maintained by the community at \fI\%https://github\.com/beanstalkd/beanstalkd/issues\fR
+208
View File
@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' content='text/html;charset=utf8'>
<meta name='generator' content='Ronn-NG/v0.8.2 (http://github.com/apjanke/ronn-ng/tree/0.8.2)'>
<title>beanstalkd(1) - simple, fast work queue</title>
<style type='text/css' media='all'>
/* style: man */
body#manpage {margin:0}
.mp {max-width:100ex;padding:0 9ex 1ex 4ex}
.mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}
.mp h2 {margin:10px 0 0 0}
.mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}
.mp h3 {margin:0 0 0 4ex}
.mp dt {margin:0;clear:left}
.mp dt.flush {float:left;width:8ex}
.mp dd {margin:0 0 0 9ex}
.mp h1,.mp h2,.mp h3,.mp h4 {clear:left}
.mp pre {margin-bottom:20px}
.mp pre+h2,.mp pre+h3 {margin-top:22px}
.mp h2+pre,.mp h3+pre {margin-top:5px}
.mp img {display:block;margin:auto}
.mp h1.man-title {display:none}
.mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}
.mp h2 {font-size:16px;line-height:1.25}
.mp h1 {font-size:20px;line-height:2}
.mp {text-align:justify;background:#fff}
.mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}
.mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}
.mp u {text-decoration:underline}
.mp code,.mp strong,.mp b {font-weight:bold;color:#131211}
.mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}
.mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}
.mp b.man-ref {font-weight:normal;color:#434241}
.mp pre {padding:0 4ex}
.mp pre code {font-weight:normal;color:#434241}
.mp h2+pre,h3+pre {padding-left:0}
ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}
ol.man-decor {width:100%}
ol.man-decor li.tl {text-align:left}
ol.man-decor li.tc {text-align:center;letter-spacing:4px}
ol.man-decor li.tr {text-align:right;float:right}
</style>
</head>
<!--
The following styles are deprecated and will be removed at some point:
div#man, div#man ol.man, div#man ol.head, div#man ol.man.
The .man-page, .man-decor, .man-head, .man-foot, .man-title, and
.man-navigation should be used instead.
-->
<body id='manpage'>
<div class='mp' id='man'>
<div class='man-navigation' style='display:none'>
<a href="#NAME">NAME</a>
<a href="#SYNOPSIS">SYNOPSIS</a>
<a href="#DESCRIPTION">DESCRIPTION</a>
<a href="#OPTIONS">OPTIONS</a>
<a href="#ENVIRONMENT">ENVIRONMENT</a>
<a href="#SEE-ALSO">SEE ALSO</a>
<a href="#AUTHOR">AUTHOR</a>
</div>
<ol class='man-decor man-head man head'>
<li class='tl'>beanstalkd(1)</li>
<li class='tc'></li>
<li class='tr'>beanstalkd(1)</li>
</ol>
<h2 id="NAME">NAME</h2>
<p class="man-name">
<code>beanstalkd</code> - <span class="man-whatis">simple, fast work queue</span>
</p>
<h2 id="SYNOPSIS">SYNOPSIS</h2>
<p><code>beanstalkd</code> [<var>options</var>]</p>
<h2 id="DESCRIPTION">DESCRIPTION</h2>
<p><code>Beanstalkd</code> is a simple work-queue service. Its interface is
generic, though it was originally designed for reducing the latency of
page views in high-volume web applications by running time-consuming
tasks asynchronously.</p>
<p>When started, <code>beanstalkd</code> opens a socket (or uses a file descriptor
provided by the <span class="man-ref">init<span class="s">(1)</span></span> system, see <a href="#ENVIRONMENT" title="ENVIRONMENT" data-bare-link="true">ENVIRONMENT</a>) and listens for
incoming connections. For each connection, it reads a sequence of
commands to create, reserve, delete, and otherwise manipulate "jobs",
units of work to be done. See file <code>doc/protocol.txt</code> in the
<code>beanstalkd</code> distribution for a thorough description of the meaning
and format of the <code>beanstalkd</code> protocol.</p>
<h2 id="OPTIONS">OPTIONS</h2>
<dl>
<dt>
<code>-b</code> <var>path</var>
</dt>
<dd>Use a binlog to keep jobs on persistent storage in directory <var>path</var>.
Upon startup, <code>beanstalkd</code> will recover any binlog that is present
in <var>path</var>, then, during normal operation, append new jobs and
changes in state to the binlog.</dd>
<dt>
<code>-f</code> <var>ms</var>
</dt>
<dd>Call <span class="man-ref">fsync<span class="s">(2)</span></span> at most once every <var>ms</var> milliseconds. Larger values
for <var>ms</var> reduce disk activity and improve speed at the cost of
safety. A power failure could result in the loss of up to <var>ms</var>
milliseconds of history.
<p>A <var>ms</var> value of 0 will cause <code>beanstalkd</code> to call fsync every time
it writes to the binlog.</p>
<p>The default behavior is to sync every 50 ms.</p>
<p>(This option has no effect without <code>-b</code>.)</p>
</dd>
<dt><code>-F</code></dt>
<dd>Never call <span class="man-ref">fsync<span class="s">(2)</span></span>. Equivalent to <code>-f</code> with an infinite <var>ms</var> value.
<p>(This option has no effect without <code>-b</code>.)</p>
</dd>
<dt><code>-h</code></dt>
<dd>Show a brief help message and exit.</dd>
<dt>
<code>-l</code> <var>addr</var>
</dt>
<dd>Listen on address <var>addr</var> (default is 0.0.0.0).
<p>When <var>addr</var> starts with "unix:", the unprefixed value of it will be
used as the local filesystem path to create a UNIX socket instead of
a TCP socket. In this case the value of <code>-p</code> will be ignored.</p>
<p>(Option <code>-l</code> has no effect if <span class="man-ref">sd-daemon<span class="s">(5)</span></span> socket activation is
being used. See also <a href="#ENVIRONMENT" title="ENVIRONMENT" data-bare-link="true">ENVIRONMENT</a>.)</p>
</dd>
<dt>
<code>-p</code> <var>port</var>
</dt>
<dd>Listen on TCP port <var>port</var> (default is 11300).
<p>(Option <code>-p</code> has no effect if <span class="man-ref">sd-daemon<span class="s">(5)</span></span> socket activation is
being used. See also <a href="#ENVIRONMENT" title="ENVIRONMENT" data-bare-link="true">ENVIRONMENT</a>.)</p>
</dd>
<dt>
<code>-s</code> <var>bytes</var>
</dt>
<dd>The size in bytes of each binlog file.
<p>(This option has no effect without <code>-b</code>.)</p>
</dd>
<dt>
<code>-u</code> <var>user</var>
</dt>
<dd>Become the user <var>user</var> and its primary group.</dd>
<dt><code>-V</code></dt>
<dd>Increase verbosity. May be used more than once to produce more
verbose output. The output format is subject to change.</dd>
<dt><code>-v</code></dt>
<dd>Print the version string and exit.</dd>
<dt>
<code>-z</code> <var>bytes</var>
</dt>
<dd>The maximum size in bytes of a job.</dd>
<dt><code>-c</code></dt>
<dd>This flag has no effect. It is kept for historical compatibility only.</dd>
<dt><code>-n</code></dt>
<dd>This flag has no effect. It is kept for historical compatibility only.</dd>
</dl>
<h2 id="ENVIRONMENT">ENVIRONMENT</h2>
<dl>
<dt>
<code>LISTEN_PID</code>, <code>LISTEN_FDS</code>
</dt>
<dd>These variables can be set by <span class="man-ref">init<span class="s">(1)</span></span>. See <span class="man-ref">sd_listen_fds<span class="s">(3)</span></span> for
details.</dd>
</dl>
<h2 id="SEE-ALSO">SEE ALSO</h2>
<p><span class="man-ref">sd-daemon<span class="s">(3)</span></span>, <span class="man-ref">sd_listen_fds<span class="s">(3)</span></span></p>
<p>Files <code>README.md</code> and <code>doc/protocol.txt</code> in the <code>beanstalkd</code>
distribution.</p>
<p><a href="https://beanstalkd.github.io/" data-bare-link="true">https://beanstalkd.github.io/</a></p>
<h2 id="AUTHOR">AUTHOR</h2>
<p><code>Beanstalkd</code> is written by Keith Rarick and maintained by the community at
<a href="https://github.com/beanstalkd/beanstalkd/issues" data-bare-link="true">https://github.com/beanstalkd/beanstalkd/issues</a></p>
<ol class='man-decor man-foot man foot'>
<li class='tl'></li>
<li class='tc'>June 2020</li>
<li class='tr'>beanstalkd(1)</li>
</ol>
</div>
</body>
</html>
+110
View File
@@ -0,0 +1,110 @@
beanstalkd(1) -- simple, fast work queue
========================================
## SYNOPSIS
`beanstalkd` [<options>]
## DESCRIPTION
`Beanstalkd` is a simple work-queue service. Its interface is
generic, though it was originally designed for reducing the latency of
page views in high-volume web applications by running time-consuming
tasks asynchronously.
When started, `beanstalkd` opens a socket (or uses a file descriptor
provided by the init(1) system, see [ENVIRONMENT][]) and listens for
incoming connections. For each connection, it reads a sequence of
commands to create, reserve, delete, and otherwise manipulate "jobs",
units of work to be done. See file `doc/protocol.txt` in the
`beanstalkd` distribution for a thorough description of the meaning
and format of the `beanstalkd` protocol.
## OPTIONS
* `-b` <path>:
Use a binlog to keep jobs on persistent storage in directory <path>.
Upon startup, `beanstalkd` will recover any binlog that is present
in <path>, then, during normal operation, append new jobs and
changes in state to the binlog.
* `-f` <ms>:
Call fsync(2) at most once every <ms> milliseconds. Larger values
for <ms> reduce disk activity and improve speed at the cost of
safety. A power failure could result in the loss of up to <ms>
milliseconds of history.
A <ms> value of 0 will cause `beanstalkd` to call fsync every time
it writes to the binlog.
The default behavior is to sync every 50 ms.
(This option has no effect without `-b`.)
* `-F`:
Never call fsync(2). Equivalent to `-f` with an infinite <ms> value.
(This option has no effect without `-b`.)
* `-h`:
Show a brief help message and exit.
* `-l` <addr>:
Listen on address <addr> (default is 0.0.0.0).
When <addr> starts with "unix:", the unprefixed value of it will be
used as the local filesystem path to create a UNIX socket instead of
a TCP socket. In this case the value of `-p` will be ignored.
(Option `-l` has no effect if sd-daemon(5) socket activation is
being used. See also [ENVIRONMENT][].)
* `-p` <port>:
Listen on TCP port <port> (default is 11300).
(Option `-p` has no effect if sd-daemon(5) socket activation is
being used. See also [ENVIRONMENT][].)
* `-s` <bytes>:
The size in bytes of each binlog file.
(This option has no effect without `-b`.)
* `-u` <user>:
Become the user <user> and its primary group.
* `-V`:
Increase verbosity. May be used more than once to produce more
verbose output. The output format is subject to change.
* `-v`:
Print the version string and exit.
* `-z` <bytes>:
The maximum size in bytes of a job.
* `-c`:
This flag has no effect. It is kept for historical compatibility only.
* `-n`:
This flag has no effect. It is kept for historical compatibility only.
## ENVIRONMENT
* `LISTEN_PID`, `LISTEN_FDS`:
These variables can be set by init(1). See sd_listen_fds(3) for
details.
## SEE ALSO
sd-daemon(3), sd_listen_fds(3)
Files `README.md` and `doc/protocol.txt` in the `beanstalkd`
distribution.
<https://beanstalkd.github.io/>
## AUTHOR
`Beanstalkd` is written by Keith Rarick and maintained by the community at
<https://github.com/beanstalkd/beanstalkd/issues>
+746
View File
@@ -0,0 +1,746 @@
= Beanstalk Protocol =
Protocol
--------
The beanstalk protocol runs over TCP using ASCII encoding. Clients connect,
send commands and data, wait for responses, and close the connection. For each
connection, the server processes commands serially in the order in which they
were received and sends responses in the same order. All integers in the
protocol are formatted in decimal and (unless otherwise indicated)
nonnegative.
Names, in this protocol, are ASCII strings. They may contain letters (A-Z and
a-z), numerals (0-9), hyphen ("-"), plus ("+"), slash ("/"), semicolon (";"),
dot ("."), dollar-sign ("$"), underscore ("_"), and parentheses ("(" and ")"),
but they may not begin with a hyphen. They are terminated by white space
(either a space char or end of line). Each name must be at least one character
long.
The protocol contains two kinds of data: text lines and unstructured chunks of
data. Text lines are used for client commands and server responses. Chunks are
used to transfer job bodies and stats information. Each job body is an opaque
sequence of bytes. The server never inspects or modifies a job body and always
sends it back in its original form. It is up to the clients to agree on a
meaningful interpretation of job bodies.
The client may issue the "quit" command, or simply close the TCP connection
when it no longer has use for the server. However, beanstalkd performs very
well with a large number of open connections, so it is usually better for the
client to keep its connection open and reuse it as much as possible. This also
avoids the overhead of establishing new TCP connections.
If a client violates the protocol (such as by sending a request that is not
well-formed or a command that does not exist) or if the server has an error,
the server will reply with one of the following error messages:
- "OUT_OF_MEMORY\r\n" The server cannot allocate enough memory for the job.
The client should try again later.
- "INTERNAL_ERROR\r\n" This indicates a bug in the server. It should never
happen. If it does happen, please report it at
http://groups.google.com/group/beanstalk-talk.
- "BAD_FORMAT\r\n" The client sent a command line that was not well-formed.
This can happen if the line's length exceeds 224 bytes including \r\n,
if the name of a tube exceeds 200 bytes, if non-numeric
characters occur where an integer is expected, if the wrong number of
arguments are present, or if the command line is mal-formed in any other
way.
- "UNKNOWN_COMMAND\r\n" The client sent a command that the server does not
know.
These error responses will not be listed in this document for individual
commands in the following sections, but they are implicitly included in the
description of all commands. Clients should be prepared to receive an error
response after any command.
As a last resort, if the server has a serious error that prevents it from
continuing service to the current client, the server will close the
connection.
Job Lifecycle
-------------
A job in beanstalk gets created by a client with the "put" command. During its
life it can be in one of four states: "ready", "reserved", "delayed", or
"buried". After the put command, a job typically starts out ready. It waits in
the ready queue until a worker comes along and runs the "reserve" command. If
this job is next in the queue, it will be reserved for the worker. The worker
will execute the job; when it is finished the worker will send a "delete"
command to delete the job.
Here is a picture of the typical job lifecycle:
put reserve delete
-----> [READY] ---------> [RESERVED] --------> *poof*
Here is a picture with more possibilities:
put with delay release with delay
----------------> [DELAYED] <------------.
| |
| (time passes) |
| |
put v reserve | delete
-----------------> [READY] ---------> [RESERVED] --------> *poof*
^ ^ | |
| \ release | |
| `-------------' |
| |
| kick |
| |
| bury |
[BURIED] <---------------'
|
| delete
`--------> *poof*
The system has one or more tubes. Each tube consists of a ready queue and a
delay queue. Each job spends its entire life in one tube. Consumers can show
interest in tubes by sending the "watch" command; they can show disinterest by
sending the "ignore" command. This set of interesting tubes is said to be a
consumer's "watch list". When a client reserves a job, it may come from any of
the tubes in its watch list.
When a client connects, its watch list is initially just the tube named
"default". If it submits jobs without having sent a "use" command, they will
live in the tube named "default".
Tubes are created on demand whenever they are referenced. If a tube is empty
(that is, it contains no ready, delayed, or buried jobs) and no client refers
to it, it will be deleted.
Producer Commands
-----------------
The "put" command is for any process that wants to insert a job into the queue.
It comprises a command line followed by the job body:
put <pri> <delay> <ttr> <bytes>\r\n
<data>\r\n
It inserts a job into the client's currently used tube (see the "use" command
below).
- <pri> is an integer < 2**32. Jobs with smaller priority values will be
scheduled before jobs with larger priorities. The most urgent priority is 0;
the least urgent priority is 4,294,967,295.
- <delay> is an integer number of seconds to wait before putting the job in
the ready queue. The job will be in the "delayed" state during this time.
Maximum delay is 2**32-1.
- <ttr> -- time to run -- is an integer number of seconds to allow a worker
to run this job. This time is counted from the moment a worker reserves
this job. If the worker does not delete, release, or bury the job within
<ttr> seconds, the job will time out and the server will release the job.
The minimum ttr is 1. If the client sends 0, the server will silently
increase the ttr to 1. Maximum ttr is 2**32-1.
- <bytes> is an integer indicating the size of the job body, not including the
trailing "\r\n". This value must be less than max-job-size (default: 2**16).
- <data> is the job body -- a sequence of bytes of length <bytes> from the
previous line.
After sending the command line and body, the client waits for a reply, which
may be:
- "INSERTED <id>\r\n" to indicate success.
- <id> is the integer id of the new job
- "BURIED <id>\r\n" if the server ran out of memory trying to grow the
priority queue data structure.
- <id> is the integer id of the new job
- "EXPECTED_CRLF\r\n" The job body must be followed by a CR-LF pair, that is,
"\r\n". These two bytes are not counted in the job size given by the client
in the put command line.
- "JOB_TOO_BIG\r\n" The client has requested to put a job with a body larger
than max-job-size bytes.
- "DRAINING\r\n" This means that the server has been put into "drain mode" and
is no longer accepting new jobs. The client should try another server or
disconnect and try again later. To put the server in drain mode, send the
SIGUSR1 signal to the process.
The "use" command is for producers. Subsequent put commands will put jobs into
the tube specified by this command. If no use command has been issued, jobs
will be put into the tube named "default".
use <tube>\r\n
- <tube> is a name at most 200 bytes. It specifies the tube to use. If the
tube does not exist, it will be created.
The only reply is:
USING <tube>\r\n
- <tube> is the name of the tube now being used.
Worker Commands
---------------
A process that wants to consume jobs from the queue uses "reserve", "delete",
"release", and "bury". The first worker command, "reserve", looks like this:
reserve\r\n
Alternatively, you can specify a timeout as follows:
reserve-with-timeout <seconds>\r\n
This will return a newly-reserved job. If no job is available to be reserved,
beanstalkd will wait to send a response until one becomes available. Once a
job is reserved for the client, the client has limited time to run (TTR) the
job before the job times out. When the job times out, the server will put the
job back into the ready queue. Both the TTR and the actual time left can be
found in response to the stats-job command.
If more than one job is ready, beanstalkd will choose the one with the
smallest priority value. Within each priority, it will choose the one that
was received first.
A timeout value of 0 will cause the server to immediately return either a
response or TIMED_OUT. A positive value of timeout will limit the amount of
time the client will block on the reserve request until a job becomes
available.
During the TTR of a reserved job, the last second is kept by the server as a
safety margin, during which the client will not be made to wait for another
job. If the client issues a reserve command during the safety margin, or if
the safety margin arrives while the client is waiting on a reserve command,
the server will respond with:
DEADLINE_SOON\r\n
This gives the client a chance to delete or release its reserved job before
the server automatically releases it.
TIMED_OUT\r\n
If a non-negative timeout was specified and the timeout exceeded before a job
became available, or if the client's connection is half-closed, the server
will respond with TIMED_OUT.
Otherwise, the only other response to this command is a successful reservation
in the form of a text line followed by the job body:
RESERVED <id> <bytes>\r\n
<data>\r\n
- <id> is the job id -- an integer unique to this job in this instance of
beanstalkd.
- <bytes> is an integer indicating the size of the job body, not including
the trailing "\r\n".
- <data> is the job body -- a sequence of bytes of length <bytes> from the
previous line. This is a verbatim copy of the bytes that were originally
sent to the server in the put command for this job.
A job can be reserved by its id. Once a job is reserved for the client,
the client has limited time to run (TTR) the job before the job times out.
When the job times out, the server will put the job back into the ready queue.
The command looks like this:
reserve-job <id>\r\n
- <id> is the job id to reserve
This should immediately return one of these responses:
- "NOT_FOUND\r\n" if the job does not exist or reserved by a client or
is not either ready, buried or delayed.
- "RESERVED <id> <bytes>\r\n<data>\r\n". See the description for
the reserve command.
The delete command removes a job from the server entirely. It is normally used
by the client when the job has successfully run to completion. A client can
delete jobs that it has reserved, ready jobs, delayed jobs, and jobs that are
buried. The delete command looks like this:
delete <id>\r\n
- <id> is the job id to delete.
The client then waits for one line of response, which may be:
- "DELETED\r\n" to indicate success.
- "NOT_FOUND\r\n" if the job does not exist or is not either reserved by the
client, ready, or buried. This could happen if the job timed out before the
client sent the delete command.
The release command puts a reserved job back into the ready queue (and marks
its state as "ready") to be run by any client. It is normally used when the job
fails because of a transitory error. It looks like this:
release <id> <pri> <delay>\r\n
- <id> is the job id to release.
- <pri> is a new priority to assign to the job.
- <delay> is an integer number of seconds to wait before putting the job in
the ready queue. The job will be in the "delayed" state during this time.
The client expects one line of response, which may be:
- "RELEASED\r\n" to indicate success.
- "BURIED\r\n" if the server ran out of memory trying to grow the priority
queue data structure.
- "NOT_FOUND\r\n" if the job does not exist or is not reserved by the client.
The bury command puts a job into the "buried" state. Buried jobs are put into a
FIFO linked list and will not be touched by the server again until a client
kicks them with the "kick" command.
The bury command looks like this:
bury <id> <pri>\r\n
- <id> is the job id to bury.
- <pri> is a new priority to assign to the job.
There are two possible responses:
- "BURIED\r\n" to indicate success.
- "NOT_FOUND\r\n" if the job does not exist or is not reserved by the client.
The "touch" command allows a worker to request more time to work on a job.
This is useful for jobs that potentially take a long time, but you still want
the benefits of a TTR pulling a job away from an unresponsive worker. A worker
may periodically tell the server that it's still alive and processing a job
(e.g. it may do this on DEADLINE_SOON). The command postpones the auto
release of a reserved job until TTR seconds from when the command is issued.
The touch command looks like this:
touch <id>\r\n
- <id> is the ID of a job reserved by the current connection.
There are two possible responses:
- "TOUCHED\r\n" to indicate success.
- "NOT_FOUND\r\n" if the job does not exist or is not reserved by the client.
The "watch" command adds the named tube to the watch list for the current
connection. A reserve command will take a job from any of the tubes in the
watch list. For each new connection, the watch list initially consists of one
tube, named "default".
watch <tube>\r\n
- <tube> is a name at most 200 bytes. It specifies a tube to add to the watch
list. If the tube doesn't exist, it will be created.
The reply is:
WATCHING <count>\r\n
- <count> is the integer number of tubes currently in the watch list.
The "ignore" command is for consumers. It removes the named tube from the
watch list for the current connection.
ignore <tube>\r\n
The reply is one of:
- "WATCHING <count>\r\n" to indicate success.
- <count> is the integer number of tubes currently in the watch list.
- "NOT_IGNORED\r\n" if the client attempts to ignore the only tube in its
watch list.
Other Commands
--------------
The peek commands let the client inspect a job in the system. There are four
variations. All but the first operate only on the currently used tube.
- "peek <id>\r\n" - return job <id>.
- "peek-ready\r\n" - return the next ready job.
- "peek-delayed\r\n" - return the delayed job with the shortest delay left.
- "peek-buried\r\n" - return the next job in the list of buried jobs.
There are two possible responses, either a single line:
- "NOT_FOUND\r\n" if the requested job doesn't exist or there are no jobs in
the requested state.
Or a line followed by a chunk of data, if the command was successful:
FOUND <id> <bytes>\r\n
<data>\r\n
- <id> is the job id.
- <bytes> is an integer indicating the size of the job body, not including
the trailing "\r\n".
- <data> is the job body -- a sequence of bytes of length <bytes> from the
previous line.
The kick command applies only to the currently used tube. It moves jobs into
the ready queue. If there are any buried jobs, it will only kick buried jobs.
Otherwise it will kick delayed jobs. It looks like:
kick <bound>\r\n
- <bound> is an integer upper bound on the number of jobs to kick. The server
will kick no more than <bound> jobs.
The response is of the form:
KICKED <count>\r\n
- <count> is an integer indicating the number of jobs actually kicked.
The kick-job command is a variant of kick that operates with a single job
identified by its job id. If the given job id exists and is in a buried or
delayed state, it will be moved to the ready queue of the the same tube where it
currently belongs. The syntax is:
kick-job <id>\r\n
- <id> is the job id to kick.
The response is one of:
- "NOT_FOUND\r\n" if the job does not exist or is not in a kickable state. This
can also happen upon internal errors.
- "KICKED\r\n" when the operation succeeded.
The stats-job command gives statistical information about the specified job if
it exists. Its form is:
stats-job <id>\r\n
- <id> is a job id.
The response is one of:
- "NOT_FOUND\r\n" if the job does not exist.
- "OK <bytes>\r\n<data>\r\n"
- <bytes> is the size of the following data section in bytes.
- <data> is a sequence of bytes of length <bytes> from the previous line. It
is a YAML file with statistical information represented by a dictionary.
The stats-job data is a YAML file representing a single dictionary of string
keys to scalar values. It contains these keys:
- "id" is the job id
- "tube" is the name of the tube that contains this job
- "state" is "ready" or "delayed" or "reserved" or "buried"
- "pri" is the priority value set by the put, release, or bury commands.
- "age" is the time in seconds since the put command that created this job.
- "delay" is the integer number of seconds to wait before putting this job in
the ready queue.
- "ttr" -- time to run -- is the integer number of seconds a worker is
allowed to run this job.
- "time-left" is the number of seconds left until the server puts this job
into the ready queue. This number is only meaningful if the job is
reserved or delayed. If the job is reserved and this amount of time
elapses before its state changes, it is considered to have timed out.
- "file" is the number of the earliest binlog file containing this job.
If -b wasn't used, this will be 0.
- "reserves" is the number of times this job has been reserved.
- "timeouts" is the number of times this job has timed out during a
reservation.
- "releases" is the number of times a client has released this job from a
reservation.
- "buries" is the number of times this job has been buried.
- "kicks" is the number of times this job has been kicked.
The stats-tube command gives statistical information about the specified tube
if it exists. Its form is:
stats-tube <tube>\r\n
- <tube> is a name at most 200 bytes. Stats will be returned for this tube.
The response is one of:
- "NOT_FOUND\r\n" if the tube does not exist.
- "OK <bytes>\r\n<data>\r\n"
- <bytes> is the size of the following data section in bytes.
- <data> is a sequence of bytes of length <bytes> from the previous line. It
is a YAML file with statistical information represented by a dictionary.
The stats-tube data is a YAML file representing a single dictionary of string
keys to scalar values. It contains these keys:
- "name" is the tube's name.
- "current-jobs-urgent" is the number of ready jobs with priority < 1024 in
this tube.
- "current-jobs-ready" is the number of jobs in the ready queue in this tube.
- "current-jobs-reserved" is the number of jobs reserved by all clients in
this tube.
- "current-jobs-delayed" is the number of delayed jobs in this tube.
- "current-jobs-buried" is the number of buried jobs in this tube.
- "total-jobs" is the cumulative count of jobs created in this tube in
the current beanstalkd process.
- "current-using" is the number of open connections that are currently
using this tube.
- "current-waiting" is the number of open connections that have issued a
reserve command while watching this tube but not yet received a response.
- "current-watching" is the number of open connections that are currently
watching this tube.
- "pause" is the number of seconds the tube has been paused for.
- "cmd-delete" is the cumulative number of delete commands for this tube
- "cmd-pause-tube" is the cumulative number of pause-tube commands for this
tube.
- "pause-time-left" is the number of seconds until the tube is un-paused.
The stats command gives statistical information about the system as a whole.
Its form is:
stats\r\n
The server will respond:
OK <bytes>\r\n
<data>\r\n
- <bytes> is the size of the following data section in bytes.
- <data> is a sequence of bytes of length <bytes> from the previous line. It
is a YAML file with statistical information represented by a dictionary.
The stats data for the system is a YAML file representing a single dictionary
of string keys to scalar values. Entries described as "cumulative" are reset
when the beanstalkd process starts;
they are not stored on disk with the -b flag.
- "current-jobs-urgent" is the number of ready jobs with priority < 1024.
- "current-jobs-ready" is the number of jobs in the ready queue.
- "current-jobs-reserved" is the number of jobs reserved by all clients.
- "current-jobs-delayed" is the number of delayed jobs.
- "current-jobs-buried" is the number of buried jobs.
- "cmd-put" is the cumulative number of put commands.
- "cmd-peek" is the cumulative number of peek commands.
- "cmd-peek-ready" is the cumulative number of peek-ready commands.
- "cmd-peek-delayed" is the cumulative number of peek-delayed commands.
- "cmd-peek-buried" is the cumulative number of peek-buried commands.
- "cmd-reserve" is the cumulative number of reserve commands.
- "cmd-reserve-with-timeout" is the cumulative number of reserve-with-timeout commands.
- "cmd-touch" is the cumulative number of touch commands.
- "cmd-use" is the cumulative number of use commands.
- "cmd-watch" is the cumulative number of watch commands.
- "cmd-ignore" is the cumulative number of ignore commands.
- "cmd-delete" is the cumulative number of delete commands.
- "cmd-release" is the cumulative number of release commands.
- "cmd-bury" is the cumulative number of bury commands.
- "cmd-kick" is the cumulative number of kick commands.
- "cmd-stats" is the cumulative number of stats commands.
- "cmd-stats-job" is the cumulative number of stats-job commands.
- "cmd-stats-tube" is the cumulative number of stats-tube commands.
- "cmd-list-tubes" is the cumulative number of list-tubes commands.
- "cmd-list-tube-used" is the cumulative number of list-tube-used commands.
- "cmd-list-tubes-watched" is the cumulative number of list-tubes-watched
commands.
- "cmd-pause-tube" is the cumulative number of pause-tube commands.
- "job-timeouts" is the cumulative count of times a job has timed out.
- "total-jobs" is the cumulative count of jobs created.
- "max-job-size" is the maximum number of bytes in a job.
- "current-tubes" is the number of currently-existing tubes.
- "current-connections" is the number of currently open connections.
- "current-producers" is the number of open connections that have each
issued at least one put command.
- "current-workers" is the number of open connections that have each issued
at least one reserve command.
- "current-waiting" is the number of open connections that have issued a
reserve command but not yet received a response.
- "total-connections" is the cumulative count of connections.
- "pid" is the process id of the server.
- "version" is the version string of the server.
- "rusage-utime" is the cumulative user CPU time of this process in seconds
and microseconds.
- "rusage-stime" is the cumulative system CPU time of this process in
seconds and microseconds.
- "uptime" is the number of seconds since this server process started running.
- "binlog-oldest-index" is the index of the oldest binlog file needed to
store the current jobs.
- "binlog-current-index" is the index of the current binlog file being
written to. If binlog is not active this value will be 0.
- "binlog-max-size" is the maximum size in bytes a binlog file is allowed
to get before a new binlog file is opened.
- "binlog-records-written" is the cumulative number of records written
to the binlog.
- "binlog-records-migrated" is the cumulative number of records written
as part of compaction.
- "draining" is set to "true" if the server is in drain mode,
"false" otherwise.
- "id" is a random id string for this server process, generated every time
beanstalkd process starts.
- "hostname" is the hostname of the machine as determined by uname.
- "os" is the OS version as determined by uname
- "platform" is the machine architecture as determined by uname
The list-tubes command returns a list of all existing tubes. Its form is:
list-tubes\r\n
The response is:
OK <bytes>\r\n
<data>\r\n
- <bytes> is the size of the following data section in bytes.
- <data> is a sequence of bytes of length <bytes> from the previous line. It
is a YAML file containing all tube names as a list of strings.
The list-tube-used command returns the tube currently being used by the
client. Its form is:
list-tube-used\r\n
The response is:
USING <tube>\r\n
- <tube> is the name of the tube being used.
The list-tubes-watched command returns a list tubes currently being watched by
the client. Its form is:
list-tubes-watched\r\n
The response is:
OK <bytes>\r\n
<data>\r\n
- <bytes> is the size of the following data section in bytes.
- <data> is a sequence of bytes of length <bytes> from the previous line. It
is a YAML file containing watched tube names as a list of strings.
The quit command simply closes the connection. Its form is:
quit\r\n
The pause-tube command can delay any new job being reserved for a given time. Its form is:
pause-tube <tube-name> <delay>\r\n
- <tube-name> is the tube to pause
- <delay> is an integer number of seconds < 2**32 to wait before reserving any more
jobs from the queue
There are two possible responses:
- "PAUSED\r\n" to indicate success.
- "NOT_FOUND\r\n" if the tube does not exist.
+623
View File
@@ -0,0 +1,623 @@
# Beanstalkd中文协议
### 总括
`beanstalkd` 协议基于 ASCII 编码运行在 tcp 上. 客户端连接服务器并发送指令和数据,然后等待响应并关闭连接. 对于每个连接,服务器按照接收命令的序列依次处理并响应. 所有整型值都非负的十进制数,除非有特别声明.
### 名称约定
所有名称必须是 ASCII 码字符串,即包括:
* **字母** (A-Z and a-z)
* **数字** (0-9)
* **连字符** ("-")
* **加号** ("+")
* **斜线** ("/")
* **分号** (";")
* **点** (".")
* **美元符** ("$")
* **下划线** ("_")
* **括号** ("*(*" and "*)*")
**注意**:名称不能以连字符开始,并且是以空白字符结束,每个名称至少包含一个字符.
### 错误说明
| 返回的错误 | 描述 |
| --------------------------------------- | -------- |
| `OUT_OF_MEMORY\r\n` | 服务器没有足够的内存分配给特定的 job,客户端应该稍后重试 |
| `INTERNAL_ERROR\r\n` | 服务器内部错误,该错误不应该发生,如果发生了,请报告: [http://groups.google.com/group/beanstalk-talk](http://groups.google.com/group/beanstalk-talk).|
| `BAD_FORMAT\r\n` | 格式不正确,客户端发送的指令格式出错,有可能不是以 `\r\n` 结尾,或者要求整型值等等 |
| `UNKNOWN_COMMAND\r\n` | 未知的命令,客户端发送的指令服务器不理解 |
### job 的生命周期
Client 使用 put 命令创建一个工作任务 job. 在整个生命周期中 job 可能有四个工作状态:ready、reserved、delayed、buried. 在 put 操作之后,一个 job 的典型状态是 ready,在 ready 队列中,它将等待一个 worker 取出此 job 并设置为其为 reserved 状态. worker占有此 job 并执行,当 job 执行完毕,worker 可以发送一个 delete 指令删除此 job.
| Status | Description |
| --------------------| ------------- |
| `ready` | 等待被取出并处理 |
| `reserved` | 如果 job 被 worker 取出,将被此 worker 预订,worker 将执行此 job |
| `delayed` | 等待特定时间之后,状态再迁移为 `ready` 状态 |
| `buried` | 等待唤醒,通常在 job 处理失败时进入该状态
#### job 典型的生命周期
```text
put reserve delete
-----> [READY] ---------> [RESERVED] --------> *poof*
```
#### job 可能的状态迁移
```text
put with delay release with delay
----------------> [DELAYED] <------------.
| |
kick | (time passes) |
| |
put v reserve | delete
-----------------> [READY] ---------> [RESERVED] --------> *poof*
^ ^ | |
| \ release | |
| `-------------' |
| |
| kick |
| |
| bury |
[BURIED] <---------------'
|
| delete
`--------> *poof*
```
## Tubes
一个 beanstalkd 实例服务可能有一个或者多个 tube,用来储存统一类型的 job.每个 tube 由一个就绪 (`ready`) 队列与延迟 (`delayed`) 队列组成.每个 job 所有的状态迁移在一个 tube 中完成.通过发送 `watch` 指令, 消费者 consumers 可以监控感兴趣的 tube.通过发送 `ignore` 指令, 消费者 consumers 可以取消监控 tube.通过 `list-tubes-watched`命令返回所有监控的 tubes,当客户端预订 (`reserved`) 一个 job,此 job 可能来自任何一个它监控的 tube.
当一个客户端连接上服务器时,客户端监控的 tube 默认为 `defaut`,如果客户端提交 job 时,没有使用 `use` 命令,那么这些 job 就存于名为`default`的 tube 中.
tube 按需求创建,无论他们在什么时候被引用到.如果一个 tube 变为空(即 no ready jobs,no delayed jobs,no buried jobs)和没有任何客户端引用(being watched),它将会被**自动删除**.
### 指令说明(Commands)
#### 生产者指令说明(Producer Commands)
#### `put`
插入一个 job 到队列
**指令格式**
```text
put <pri> <delay> <ttr> <bytes>\r\n
<data>\r\n
```
* `<pri>` 整型值, 为优先级, 可以为0-2^32 (4,294,967,295) 值越小优先级越高, 默认为1024.
* `<delay>` 整型值,延迟`ready`的秒数,在这段时间 job 为 `delayed` 状态. 最大 delay 值为 2^32-1.
* `<ttr>` -- time to run --整型值,允许 worker 执行的最大秒数,如果 worker 在这段时间不能 delete,release,bury job,那么当 job 超时,服务器将**自动** release 此job,此 job 的状态迁移为`ready`. 最小为 1 秒,如果客户端指定为 0 将会被重置为 1. 最大 ttr 值为 2^32-1.
* `<bytes>` 整型值,job body的长度,不包含`\r\n`,这个值必须小于 `max-job-size`,默认为 2^16.
* `<data>` job body
**响应**
```text
INSERTED <id>\r\n
```
表示插入 job 成功,id 为新 job 的任务标识,整型值 (uint64)
```text
BURIED <id>\r\n
```
如服务器为了增加队列的优先级而,内存不足时返回,id 为新 job 的任务标识,整型值 (uint64)
```text
EXPECTED_CRLF\r\n
```
job body 必须以 `\r\n` 结尾
```text
JOB_TOO_BIG\r\n
```
job body 的长度超过 `max-job-size`
```text
DRAINING\r\n
```
表示服务器资源耗尽,表示服务器已经进入了`drain mode`,服务器再也不能接受连接,客户端应该使用其他服务器或者断开稍后重试
#### `use`
producer 生产者使用,之后使用的 put 指令,都将会把 job 放置于 use 的 tube 中,如果没有指定 use 的 tube, 任务 job 将会进入默认名称为 `default` 的 tube
**指令格式**
```text
use <tube>\r\n
tube tube 的名称,最大为 200 字节,不存在时将自动创建
```
**响应**
```text
USING <tube>\r\n tube 为正在使用的tube名称
```
#### 消费者指令说明(Worker Commands)
#### `reserve`
预订(reserved) job 等待处理. beanstalkd 将返回一个新预订的 job,如果没有 job,beanstalkd 将一直等待到有 job 时才发送响应. 一旦 job 状态迁移为 `reserved`, 取出 job 的 client 被限制在指定的时间(如果设置了ttr)完成,否则将超时,job 状态重装迁移为ready.
**指令格式**
```text
reserve\r\n
```
可选的一个相似的命令
`reserve-with-timeout \r\n` 设置取 job 的超时时间,timeout 设置为 0 时,服务器立即响应或者 TIMED_OUT,积极的设置超时,将会限制客户端阻塞在取 job 的请求的时间.
##### 失败响应
```text
DEADLINE_SOON\r\n
```
* 在一个预定的任务 job 的运行时间内, **最后一秒**会被服务器保持为一个**安全边际**,在这个**时间间隔** (1s) 中,client 将无法获取其他任务. 如果客户端在安全隔离期间发出一个预留 (reserve) 指令,或者客户端在等候一个预定 (reserve) 指令返回结果时,client 安全隔离期到达时,将会收到 `DEADLINE_SOON` 回复
* `DEADLINE_SOON` 的返回结果提示 client 这是一个 delete 或者 touch 它所预订(reserved) 的任务 job 的时机,之后 beanstalkd 服务端将会自动释放 `ttr` 到期的 job
```text
TIMED_OUT\r\n 超时
```
##### 成功响应
```text
RESERVED <id> <bytes>\r\n
<data>\r\n
```
成功取出 job:
* `<id>` 为 job id,整型值
* `<bytes>` 为 job body 的长度,不包含`\r\n`,
* `<data>` 为job body
#### `delete`
从队列中删除一个job
**指令格式**
```text
delete <id>\r\n
```
* `<id>` 为 job id
**响应**
```text
DELETED\r\n
```
* 删除成功
```text
NOT_FOUND\r\n
```
* job 不存在时,或者 job 并不为当前的 client 所 reserved;
* job 的状态不为 `ready`和 `buried`(这种情况是在 job 被其他 client 所预订(reserved) 且还未执行超时,此时当前 client 发送了 delete 指令就会收到 `NOT_FOUND` 回复)
#### `release`
release 指令将一个`reserved`的 job 恢复为`ready`. 它通常在 job 执行失败时使用.
**指令格式**
```text
release <id> <pri> <delay>\r\n
```
* `<id>` 为job id
* `<pri>` 为 job 的优先级
* `<delay>` 为延迟`ready`的秒数
**响应**
```text
RELEASED\r\n 表明成功
BURIED\r\n 如服务器为了增加队列的优先级而,内存不足时返回
bury <id> <pri>\r\n
```
* `<id>` 为 job id
* `<pri>` 为优先级
**响应**
```text
BURIED\r\n 表明成功
NOT_FOUND\r\n 如果 job 不存在或者 client 没有预订此 job
```
#### `touch`
允许 worker 请求更多的时间执行 job;当 job 需要更长的时间来执行,这个指令就将会起作用,worker 可用周期性的告诉服务器它仍然在执行job(可以被 `DEADLINE_SOON` 触发)
**指令格式**
```text
touch <id>\r\n
```
* `<id>` 为 job id
**响应**
```text
TOUCHED\r\n 表明成功
NOT_FOUND\r\n 如果 job 不存在或者 client 没有预订此 job
```
#### `watch`
添加监控的 tube 到 watch list 列表,reserve 指令将会从监控的 tube 列表获取 job,对于每个连接,监控的列表默认为 `default`
**指令格式**
```text
watch <tube>\r\n
```
* `<tube>` 为监控的 tube 名称,名称最大为 200 字节,如果 tube 不存在会**自动创建**
**响应**
```text
WATCHING <count>\r\n 表明成功
```
* `<count>` 整型值,已监控的 tube 数量
#### `ignore`
从已监控的 watch list 列表中移出特定的 tube
**指令格式**
```text
ignore <tube>\r\n
```
* `<tube>` 为移出的 tube 名称,名称最多为 200 字节,如果 tube 不存在会自动创建
**响应**
```text
WATCHING <count>\r\n 表明成功
```
* `<count>` 整型值,已监控的tube数量
```text
NOT_IGNORED\r\n
```
* 如果 client 尝试忽略其仅有的tube时的响应
#### 其他指令说明(Other Command)
#### `peek`
让 client 在系统中检查 job,有四种形式的命令,其中第一种形式的指令是针对当前使用 (use) 的 tube
**指令格式**
```text
peek <id>\r\n 返回 id 对应的 job
peek-ready\r\n 返回下一个 ready job
peek-delayed\r\n 返回下一个延迟剩余时间最短的 job
peek-buried\r\n 返回下一个在 buried 列表中的 job
```
**响应**
```text
NOT_FOUND\r\n
```
* 如果 job 不存在,或者没有对应状态的 job
```text
FOUND <id> <bytes>\r\n <data>\r\n
```
* `<id>` 为对应的 job id
* `<bytes>` job body 的字节数
* `<data>` 为 job body
#### `kick`
此指令应用在当前使用 (use) 的 tube 中,它将 job 的状态迁移为`ready`或者`delayed`
**指令格式**
```text
kick <bound>\r\n
```
* `<bound>` 整型值,唤醒的 job 上限
**响应**
```
KICKED <count>\r\n
```
* `<count>` 为真实唤醒的job数量
#### kick-job
kick 指令的一个变体,可以使单个 job 被唤醒,使一个状态为`buried`或者`delayed`的 job迁移为`ready`,所有的状态迁移都在相同的 tube 中完成
**指令格式**
```text
kick-job <id>\r\n
```
* `<id>` 为job id
**响应**
`NOT_FOUND\r\n` 如果 job 不存在,或者 job 是不可唤醒的状态
`KICKED\r\n` 表明成功
#### `stats-job`
统计 job 的相关信息
**指令格式**
```text
stats-job <id>\r\n
```
* `<id>` 为 job id
**响应**
```text
NOT_FOUND\r\n 如果job不存在
OK <bytes>\r\n<data>\r\n
```
* `<bytes>` 为接下来的 data 区块的长度
* `<data>` 为 YAML file 的统计信息
其中 YAML file 包括的 key 有:
* `id` 表示 job id
* `tube` 表示 tube 的名称
* `state` 表示 job 的当前状态
* `pri` 表示 job 的优先级
* `age` 表示 job 创建的时间单位秒
* `time-left` 表示 job 的状态迁移为 ready 的时间,仅在 job 状态为`reserved`或者`delayed`时有意义,当 job 状态为`reserved`时表示剩余的超时时间.
* `file` 表示包含此 job 的`binlog`序号,如果没有开启它将为 0
* `reserves` 表示 job 被`reserved`的次数
* `timeouts` 表示 job 处理的超时时间
* `releases` 表示 job 被`released`的次数
* `buries` 表示 job 被`buried`的次数
* `kicks` 表示 job 被`kiced`的次数
#### `stats-tube`
统计 tube 的相关信息
**指令格式**
```text
stats-tube <tube>\r\n
```
* `<tube>` 为对应的 tube 的名称,最多为 200 字节
**响应**
```text
NOT_FOUND\r\n
```
* 如果tube不存在
```text
OK <bytes>\r\n
<data>\r\n
```
* `<bytes>` 为接下来的 data 区块的长度
* `<data>` 为 YAML file的统计信息
其中 YAML file 包括的 key 有:
* `name` 表示tube的名称
* `current-jobs-urgent` 此 tube 中优先级小于 1024 状态为`ready`的 job 数量
* `current-jobs-ready` 此 tube 中状态为`ready`的 job 数量
* `current-jobs-reserved` 此 tube 中状态为`reserved`的 job 数量
* `current-jobs-delayed` 此 tube 中状态为`delayed`的 job 数量
* `current-jobs-bureid` 此 tube 中状态为`buried`的job数量
* `total-jobs` 此 tube 中创建的所有job数量
* `current-using` 使用此 tube 打开的连接数
* `current-wating` 使用此 tube 打开连接并且等待响应的连接数
* `current-watching` 打开的连接监控此 tube 的数量
* `pause` 此 tube 暂停的秒数
* `cmd-delete` 此 tube 中总共执行的`delete`指令的次数
* `cmd-pause-tube` 此 tube 中总共执行`pause-tube`指令的次数
* `pause-time-left` 此 tube 暂停剩余的秒数
#### `stats`
返回整个消息队列系统的整体信息
**指令格式**
```text
stats\r\n
```
**响应**
```text
OK <bytes>\r\n
<data>\r\n
```
* `<bytes>` 为接下来的 data 区块的长度
* `<data>` 为 YAML file 的统计信息
从 beanstalkd 进程启动以来,所有的信息都累积的,这些信息不储存在 binlog 中
其中 YAML file 包括的key有:
* `current-jobs-urgent` 优先级小于 1024 状态为`ready`的 job 数量
* `current-jobs-ready` 状态为`ready`的 job 数量
* `current-jobs-reserved` 状态为`reserved`的 job 数量
* `current-jobs-delayed` 状态为`delayed`的 job 数量
* `current-jobs-bureid` 状态为`buried`的 job 数量
* `cmd-put` 总共执行`put`指令的次数
* `cmd-peek` 总共执行`peek`指令的次数
* `cmd-peek-ready` 总共执行`peek-ready`指令的次数
* `cmd-peek-delayed` 总共执行`peek-delayed`指令的次数
* `cmd-peek-buried` 总共执行`peek-buried`指令的次数
* `cmd-reserve` 总共执行`reserve`指令的次数
* `cmd-use` 总共执行`use`指令的次数
* `cmd-watch` 总共执行`watch`指令的次数
* `cmd-ignore` 总共执行`ignore`指令的次数
* `cmd-release` 总共执行`release`指令的次数
* `cmd-bury` 总共执行`bury`指令的次数
* `cmd-kick` 总共执行`kick`指令的次数
* `cmd-stats` 总共执行`stats`指令的次数
* `cmd-stats-job` 总共执行`stats-job`指令的次数
* `cmd-stats-tube` 总共执行`stats-tube`指令的次数
* `cmd-list-tubes` 总共执行`list-tubes`指令的次数
* `cmd-list-tube-used` 总共执行`list-tube-used`指令的次数
* `cmd-list-tubes-watched` 总共执行`list-tubes-watched`指令的次数
* `cmd-pause-tube` 总共执行`pause-tube`指令的次数
* `job-timeouts` 所有超时的 job 的总共数量
* `total-jobs` 创建的所有 job 数量
* `max-job-size` job 的数据部分最大长度
* `current-tubes` 当前存在的 tube 数量
* `current-connections` 当前打开的连接数
* `current-producers` 当前所有的打开的连接中至少执行一次 put 指令的连接数量
* `current-workers` 当前所有的打开的连接中至少执行一次 reserve 指令的连接数量
* `current-waiting` 当前所有的打开的连接中执行 reserve 指令但是未响应的连接数量
* `total-connections` 总共处理的连接数
* `pid` 服务器进程的 id
* `version` 服务器版本号
* `rusage-utime` 进程总共占用的用户 CPU 时间
* `rusage-stime` 进程总共占用的系统 CPU 时间
* `uptime` 服务器进程运行的秒数
* `binlog-oldest-index` 开始储存 jobs 的 binlog 索引号
* `binlog-current-index` 当前储存 jobs 的 binlog 索引号
* `binlog-max-size` binlog 的最大容量
* `binlog-records-written` binlog 累积写入的记录数
* `binlog-records-migrated` is the cumulative number of records written as part of compaction.
* `id` 一个随机字符串,在 beanstalkd 进程启动时产生
* `hostname` 主机名
#### `list-tubes`
列出当前 beanstalkd 所有存在的 tubes
**指令格式**
```text
list-tubes\r\n
```
**响应**
```text
OK <bytes>\r\n
<data>\r\n
```
* `<bytes>` 为接下来的 data 区块的长度
* `<data>` 为 YAML file,包含所有的 tube 名称
#### `list-tube-used`
列出当前 client 正在 use 的 tube
**指令格式**
```text
list-tube-used\r\n
```
**响应**
```text
USING <tube>\r\n
```
* `<tube>` 为 tube 名称
#### `list-tubes-watched`
列出当前 client 所 watch 的 tubes
**指令格式**
```text
list-tubes-watched\r\n
```
**响应**
```text
OK <bytes>\r\n
<data>\r\n
```
* `<bytes>` 为接下来的 data 区块的长度
* `<data>` 为 YAML file,包含所有的 tube 名称
#### `quit`
client 向 beanstalkd 发送 `quit` 报文,并关闭连接,beanstalkd 收到该报文后主动关闭连接
**指令格式**
```text
quit\r\n
```
无响应
#### `pause-tube`
此指令针对特定的 tube 内所有新的 job 延迟指定的秒数
**指令格式**
```text
pause-tube <tube-name> <delay>\r\n
```
* `<delay>` 延迟的时间
**响应**
```text
PAUSED\r\n 表示成功
NOT_FOUND\r\n tube 不存在
```
> Translated by PHPBoy :http://www.phpboy.net/ and fzb.me
Revised by Pseudocodes: https://github.com/pseudocodes
+596
View File
@@ -0,0 +1,596 @@
#include "dat.h"
#include <stdint.h>
#include <inttypes.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
static int readrec(File*, Job *, int*);
static int readrec5(File*, Job *, int*);
static int readfull(File*, void*, int, int*, char*);
static void warnpos(File*, int, char*, ...)
__attribute__((format(printf, 3, 4)));
FAlloc *falloc = &rawfalloc;
enum
{
Walver5 = 5
};
typedef struct Jobrec5 Jobrec5;
struct Jobrec5 {
uint64 id;
uint32 pri;
uint64 delay; // usec
uint64 ttr; // usec
int32 body_size;
uint64 created_at; // usec
uint64 deadline_at; // usec
uint32 reserve_ct;
uint32 timeout_ct;
uint32 release_ct;
uint32 bury_ct;
uint32 kick_ct;
byte state;
char pad[1];
};
enum
{
Jobrec5size = offsetof(Jobrec5, pad)
};
// rawfalloc allocates disk space of len bytes.
// It expects fd's offset to be 0; may also reset fd's offset to 0.
// Returns 0 on success, and a positive errno otherwise.
int
rawfalloc(int fd, int len)
{
// We do not use ftruncate() because it might extend the file
// with a sequence of null bytes or a hole.
// posix_fallocate() is not portable enough, might fail for NFS.
static char buf[4096] = {0};
int i, w;
for (i = 0; i < len; i += w) {
w = write(fd, buf, sizeof buf);
if (w == -1)
return errno;
}
lseek(fd, 0, 0); // do not care if this fails
return 0;
}
void
fileincref(File *f)
{
if (!f) return;
f->refs++;
}
void
filedecref(File *f)
{
if (!f) return;
f->refs--;
if (f->refs < 1) {
walgc(f->w);
}
}
void
fileaddjob(File *f, Job *j)
{
Job *h;
h = &f->jlist;
if (!h->fprev) h->fprev = h;
j->file = f;
j->fprev = h->fprev;
j->fnext = h;
h->fprev->fnext = j;
h->fprev = j;
fileincref(f);
}
void
filermjob(File *f, Job *j)
{
if (!f) return;
if (f != j->file) return;
j->fnext->fprev = j->fprev;
j->fprev->fnext = j->fnext;
j->fnext = 0;
j->fprev = 0;
j->file = NULL;
f->w->alive -= j->walused;
j->walused = 0;
filedecref(f);
}
// Fileread reads jobs from f->path into list.
// It returns 0 on success, or 1 if any errors occurred.
int
fileread(File *f, Job *list)
{
int err = 0, v;
if (!readfull(f, &v, sizeof(v), &err, "version")) {
return err;
}
switch (v) {
case Walver:
fileincref(f);
while (readrec(f, list, &err));
filedecref(f);
return err;
case Walver5:
fileincref(f);
while (readrec5(f, list, &err));
filedecref(f);
return err;
}
warnx("%s: unknown version: %d", f->path, v);
return 1;
}
// Readrec reads a record from f->fd into linked list l.
// If an error occurs, it sets *err to 1.
// Readrec returns the number of records read, either 1 or 0.
static int
readrec(File *f, Job *l, int *err)
{
int r, sz = 0;
int namelen;
Jobrec jr;
Job *j;
Tube *t;
char tubename[MAX_TUBE_NAME_LEN];
r = read(f->fd, &namelen, sizeof(int));
if (r == -1) {
twarn("read");
warnpos(f, 0, "error");
*err = 1;
return 0;
}
if (r != sizeof(int)) {
return 0;
}
sz += r;
if (namelen >= MAX_TUBE_NAME_LEN) {
warnpos(f, -r, "namelen %d exceeds maximum of %d", namelen, MAX_TUBE_NAME_LEN - 1);
*err = 1;
return 0;
}
if (namelen < 0) {
warnpos(f, -r, "namelen %d is negative", namelen);
*err = 1;
return 0;
}
if (namelen) {
r = readfull(f, tubename, namelen, err, "tube name");
if (!r) {
return 0;
}
sz += r;
}
tubename[namelen] = '\0';
r = readfull(f, &jr, sizeof(Jobrec), err, "job struct");
if (!r) {
return 0;
}
sz += r;
// are we reading trailing zeroes?
if (!jr.id) return 0;
j = job_find(jr.id);
if (!(j || namelen)) {
// We read a short record without having seen a
// full record for this job, so the full record
// was in an earlier file that has been deleted.
// Therefore the job itself has either been
// deleted or migrated; either way, this record
// should be ignored.
return 1;
}
switch (jr.state) {
case Reserved:
jr.state = Ready;
/* Falls through */
case Ready:
case Buried:
case Delayed:
if (!j) {
if ((size_t)jr.body_size > job_data_size_limit) {
warnpos(f, -r, "job %"PRIu64" is too big (%"PRId32" > %zu)",
jr.id,
jr.body_size,
job_data_size_limit);
goto Error;
}
t = tube_find_or_make(tubename);
j = make_job_with_id(jr.pri, jr.delay, jr.ttr, jr.body_size,
t, jr.id);
job_list_reset(j);
j->r.created_at = jr.created_at;
}
j->r = jr;
job_list_insert(l, j);
// full record; read the job body
if (namelen) {
if (jr.body_size != j->r.body_size) {
warnpos(f, -r, "job %"PRIu64" size changed", j->r.id);
warnpos(f, -r, "was %d, now %d", j->r.body_size, jr.body_size);
goto Error;
}
r = readfull(f, j->body, j->r.body_size, err, "job body");
if (!r) {
goto Error;
}
sz += r;
// since this is a full record, we can move
// the file pointer and decref the old
// file, if any
filermjob(j->file, j);
fileaddjob(f, j);
}
j->walused += sz;
f->w->alive += sz;
return 1;
case Invalid:
if (j) {
job_list_remove(j);
filermjob(j->file, j);
job_free(j);
}
return 1;
}
Error:
*err = 1;
if (j) {
job_list_remove(j);
filermjob(j->file, j);
job_free(j);
}
return 0;
}
// Readrec5 is like readrec, but it reads a record in "version 5"
// of the log format.
static int
readrec5(File *f, Job *l, int *err)
{
int r, sz = 0;
size_t namelen;
Jobrec5 jr;
Job *j;
Tube *t;
char tubename[MAX_TUBE_NAME_LEN];
r = read(f->fd, &namelen, sizeof(namelen));
if (r == -1) {
twarn("read");
warnpos(f, 0, "error");
*err = 1;
return 0;
}
if (r != sizeof(namelen)) {
return 0;
}
sz += r;
if (namelen >= MAX_TUBE_NAME_LEN) {
warnpos(f, -r, "namelen %zu exceeds maximum of %d", namelen, MAX_TUBE_NAME_LEN - 1);
*err = 1;
return 0;
}
if (namelen) {
r = readfull(f, tubename, namelen, err, "v5 tube name");
if (!r) {
return 0;
}
sz += r;
}
tubename[namelen] = '\0';
r = readfull(f, &jr, Jobrec5size, err, "v5 job struct");
if (!r) {
return 0;
}
sz += r;
// are we reading trailing zeroes?
if (!jr.id) return 0;
j = job_find(jr.id);
if (!(j || namelen)) {
// We read a short record without having seen a
// full record for this job, so the full record
// was in an eariler file that has been deleted.
// Therefore the job itself has either been
// deleted or migrated; either way, this record
// should be ignored.
return 1;
}
switch (jr.state) {
case Reserved:
jr.state = Ready;
/* Falls through */
case Ready:
case Buried:
case Delayed:
if (!j) {
if ((size_t)jr.body_size > job_data_size_limit) {
warnpos(f, -r, "job %"PRIu64" is too big (%"PRId32" > %zu)",
jr.id,
jr.body_size,
job_data_size_limit);
goto Error;
}
t = tube_find_or_make(tubename);
j = make_job_with_id(jr.pri, jr.delay, jr.ttr, jr.body_size,
t, jr.id);
job_list_reset(j);
}
j->r.id = jr.id;
j->r.pri = jr.pri;
j->r.delay = jr.delay * 1000; // us => ns
j->r.ttr = jr.ttr * 1000; // us => ns
j->r.body_size = jr.body_size;
j->r.created_at = jr.created_at * 1000; // us => ns
j->r.deadline_at = jr.deadline_at * 1000; // us => ns
j->r.reserve_ct = jr.reserve_ct;
j->r.timeout_ct = jr.timeout_ct;
j->r.release_ct = jr.release_ct;
j->r.bury_ct = jr.bury_ct;
j->r.kick_ct = jr.kick_ct;
j->r.state = jr.state;
job_list_insert(l, j);
// full record; read the job body
if (namelen) {
if (jr.body_size != j->r.body_size) {
warnpos(f, -r, "job %"PRIu64" size changed", j->r.id);
warnpos(f, -r, "was %"PRId32", now %"PRId32, j->r.body_size, jr.body_size);
goto Error;
}
r = readfull(f, j->body, j->r.body_size, err, "v5 job body");
if (!r) {
goto Error;
}
sz += r;
// since this is a full record, we can move
// the file pointer and decref the old
// file, if any
filermjob(j->file, j);
fileaddjob(f, j);
}
j->walused += sz;
f->w->alive += sz;
return 1;
case Invalid:
if (j) {
job_list_remove(j);
filermjob(j->file, j);
job_free(j);
}
return 1;
}
Error:
*err = 1;
if (j) {
job_list_remove(j);
filermjob(j->file, j);
job_free(j);
}
return 0;
}
static int
readfull(File *f, void *c, int n, int *err, char *desc)
{
int r;
r = read(f->fd, c, n);
if (r == -1) {
twarn("read");
warnpos(f, 0, "error reading %s", desc);
*err = 1;
return 0;
}
if (r != n) {
warnpos(f, -r, "unexpected EOF reading %d bytes (got %d): %s", n, r, desc);
*err = 1;
return 0;
}
return r;
}
static void
warnpos(File *f, int adj, char *fmt, ...)
{
int off;
va_list ap;
off = lseek(f->fd, 0, SEEK_CUR);
fprintf(stderr, "%s:%d: ", f->path, off+adj);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
}
// Opens f for writing, writes a header, and initializes
// f->free and f->resv.
// Sets f->iswopen if successful.
void
filewopen(File *f)
{
int fd, r;
int n;
int ver = Walver;
fd = open(f->path, O_WRONLY|O_CREAT, 0400);
if (fd < 0) {
twarn("open %s", f->path);
return;
}
r = falloc(fd, f->w->filesize);
if (r) {
if (close(fd) == -1)
twarn("close");
errno = r;
twarn("falloc %s", f->path);
r = unlink(f->path);
if (r) {
twarn("unlink %s", f->path);
}
return;
}
n = write(fd, &ver, sizeof(int));
if (n < 0 || (size_t)n < sizeof(int)) {
twarn("write %s", f->path);
if (close(fd) == -1)
twarn("close");
return;
}
f->fd = fd;
f->iswopen = 1;
fileincref(f);
f->free = f->w->filesize - n;
f->resv = 0;
}
static int
filewrite(File *f, Job *j, void *buf, int len)
{
int r;
r = write(f->fd, buf, len);
if (r != len) {
twarn("write");
return 0;
}
f->w->resv -= r;
f->resv -= r;
j->walresv -= r;
j->walused += r;
f->w->alive += r;
return 1;
}
int
filewrjobshort(File *f, Job *j)
{
int r, nl;
nl = 0; // name len 0 indicates short record
r = filewrite(f, j, &nl, sizeof nl) &&
filewrite(f, j, &j->r, sizeof j->r);
if (!r) return 0;
if (j->r.state == Invalid) {
filermjob(j->file, j);
}
return r;
}
int
filewrjobfull(File *f, Job *j)
{
int nl;
fileaddjob(f, j);
nl = strlen(j->tube->name);
return
filewrite(f, j, &nl, sizeof nl) &&
filewrite(f, j, j->tube->name, nl) &&
filewrite(f, j, &j->r, sizeof j->r) &&
filewrite(f, j, j->body, j->r.body_size);
}
void
filewclose(File *f)
{
if (!f) return;
if (!f->iswopen) return;
if (f->free) {
errno = 0;
if (ftruncate(f->fd, f->w->filesize - f->free) != 0) {
twarn("ftruncate");
}
}
if (close(f->fd) == -1)
twarn("close");
f->iswopen = 0;
filedecref(f);
}
int
fileinit(File *f, Wal *w, int n)
{
f->w = w;
f->seq = n;
f->path = fmtalloc("%s/binlog.%d", w->dir, n);
return !!f->path;
}
// Adds f to the linked list in w,
// updating w->tail and w->head as necessary.
Wal*
fileadd(File *f, Wal *w)
{
if (w->tail) {
w->tail->next = f;
}
w->tail = f;
if (!w->head) {
w->head = f;
}
w->nfile++;
return w;
}
+1
View File
@@ -0,0 +1 @@
#include "darwin.c"
+112
View File
@@ -0,0 +1,112 @@
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
static void
set(Heap *h, size_t k, void *x)
{
h->data[k] = x;
h->setpos(x, k);
}
static void
swap(Heap *h, size_t a, size_t b)
{
void *tmp;
tmp = h->data[a];
set(h, a, h->data[b]);
set(h, b, tmp);
}
static int
less(Heap *h, size_t a, size_t b)
{
return h->less(h->data[a], h->data[b]);
}
static void
siftdown(Heap *h, size_t k)
{
for (;;) {
size_t p = (k-1) / 2; /* parent */
if (k == 0 || less(h, p, k)) {
return;
}
swap(h, k, p);
k = p;
}
}
static void
siftup(Heap *h, size_t k)
{
for (;;) {
size_t l = k*2 + 1; /* left child */
size_t r = k*2 + 2; /* right child */
/* find the smallest of the three */
size_t s = k;
if (l < h->len && less(h, l, s)) s = l;
if (r < h->len && less(h, r, s)) s = r;
if (s == k) {
return; /* satisfies the heap property */
}
swap(h, k, s);
k = s;
}
}
// Heapinsert inserts x into heap h according to h->less.
// It returns 1 on success, otherwise 0.
int
heapinsert(Heap *h, void *x)
{
if (h->len == h->cap) {
void **ndata;
size_t ncap = (h->len+1) * 2; /* allocate twice what we need */
ndata = malloc(sizeof(void*) * ncap);
if (!ndata) {
return 0;
}
memcpy(ndata, h->data, sizeof(void*) * h->len);
free(h->data);
h->data = ndata;
h->cap = ncap;
}
size_t k = h->len;
h->len++;
set(h, k, x);
siftdown(h, k);
return 1;
}
void *
heapremove(Heap *h, size_t k)
{
if (k >= h->len) {
return 0;
}
void *x = h->data[k];
h->len--;
set(h, k, h->data[h->len]);
siftdown(h, k);
siftup(h, k);
return x;
}
+275
View File
@@ -0,0 +1,275 @@
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
static uint64 next_id = 1;
static int cur_prime = 0;
static Job *all_jobs_init[12289] = {0};
static Job **all_jobs = all_jobs_init;
static size_t all_jobs_cap = 12289; /* == primes[0] */
static size_t all_jobs_used = 0;
static int hash_table_was_oom = 0;
static void rehash(int);
static int
_get_job_hash_index(uint64 job_id)
{
return job_id % all_jobs_cap;
}
static void
store_job(Job *j)
{
int index = 0;
index = _get_job_hash_index(j->r.id);
j->ht_next = all_jobs[index];
all_jobs[index] = j;
all_jobs_used++;
/* accept a load factor of 4 */
if (all_jobs_used > (all_jobs_cap << 2)) rehash(1);
}
static void
rehash(int is_upscaling)
{
Job **old = all_jobs;
size_t old_cap = all_jobs_cap, old_used = all_jobs_used, i;
int old_prime = cur_prime;
int d = is_upscaling ? 1 : -1;
if (cur_prime + d >= NUM_PRIMES) return;
if (cur_prime + d < 0) return;
if (is_upscaling && hash_table_was_oom) return;
cur_prime += d;
all_jobs_cap = primes[cur_prime];
all_jobs = calloc(all_jobs_cap, sizeof(Job *));
if (!all_jobs) {
twarnx("Failed to allocate %zu new hash buckets", all_jobs_cap);
hash_table_was_oom = 1;
cur_prime = old_prime;
all_jobs = old;
all_jobs_cap = old_cap;
all_jobs_used = old_used;
return;
}
all_jobs_used = 0;
hash_table_was_oom = 0;
for (i = 0; i < old_cap; i++) {
while (old[i]) {
Job *j = old[i];
old[i] = j->ht_next;
j->ht_next = NULL;
store_job(j);
}
}
if (old != all_jobs_init) {
free(old);
}
}
Job *
job_find(uint64 job_id)
{
int index = _get_job_hash_index(job_id);
Job *jh = all_jobs[index];
while (jh && jh->r.id != job_id)
jh = jh->ht_next;
return jh;
}
Job *
allocate_job(int body_size)
{
Job *j;
j = malloc(sizeof(Job) + body_size);
if (!j) {
twarnx("OOM");
return (Job *) 0;
}
memset(j, 0, sizeof(Job));
j->r.created_at = nanoseconds();
j->r.body_size = body_size;
j->body = (char *)j + sizeof(Job);
job_list_reset(j);
return j;
}
Job *
make_job_with_id(uint32 pri, int64 delay, int64 ttr,
int body_size, Tube *tube, uint64 id)
{
Job *j;
j = allocate_job(body_size);
if (!j) {
twarnx("OOM");
return (Job *) 0;
}
if (id) {
j->r.id = id;
if (id >= next_id) next_id = id + 1;
} else {
j->r.id = next_id++;
}
j->r.pri = pri;
j->r.delay = delay;
j->r.ttr = ttr;
store_job(j);
TUBE_ASSIGN(j->tube, tube);
return j;
}
static void
job_hash_free(Job *j)
{
Job **slot;
slot = &all_jobs[_get_job_hash_index(j->r.id)];
while (*slot && *slot != j) slot = &(*slot)->ht_next;
if (*slot) {
*slot = (*slot)->ht_next;
--all_jobs_used;
}
// Downscale when the hashmap is too sparse
if (all_jobs_used < (all_jobs_cap >> 4)) rehash(0);
}
void
job_free(Job *j)
{
if (j) {
TUBE_ASSIGN(j->tube, NULL);
if (j->r.state != Copy) job_hash_free(j);
}
free(j);
}
void
job_setpos(void *j, size_t pos)
{
((Job *)j)->heap_index = pos;
}
int
job_pri_less(void *ja, void *jb)
{
Job *a = (Job *)ja;
Job *b = (Job *)jb;
if (a->r.pri < b->r.pri) return 1;
if (a->r.pri > b->r.pri) return 0;
return a->r.id < b->r.id;
}
int
job_delay_less(void *ja, void *jb)
{
Job *a = ja;
Job *b = jb;
if (a->r.deadline_at < b->r.deadline_at) return 1;
if (a->r.deadline_at > b->r.deadline_at) return 0;
return a->r.id < b->r.id;
}
Job *
job_copy(Job *j)
{
if (!j)
return NULL;
Job *n = malloc(sizeof(Job) + j->r.body_size);
if (!n) {
twarnx("OOM");
return (Job *) 0;
}
memcpy(n, j, sizeof(Job) + j->r.body_size);
job_list_reset(n);
n->file = NULL; /* copies do not have refcnt on the wal */
n->tube = 0; /* Don't use memcpy for the tube, which we must refcount. */
TUBE_ASSIGN(n->tube, j->tube);
/* Mark this job as a copy so it can be appropriately freed later on */
n->r.state = Copy;
return n;
}
const char *
job_state(Job *j)
{
if (j->r.state == Ready) return "ready";
if (j->r.state == Reserved) return "reserved";
if (j->r.state == Buried) return "buried";
if (j->r.state == Delayed) return "delayed";
return "invalid";
}
// job_list_reset detaches head from the list,
// marking the list starting in head pointing to itself.
void
job_list_reset(Job *head)
{
head->prev = head;
head->next = head;
}
int
job_list_is_empty(Job *head)
{
return head->next == head && head->prev == head;
}
Job *
job_list_remove(Job *j)
{
if (!j) return NULL;
if (job_list_is_empty(j)) return NULL; /* not in a doubly-linked list */
j->next->prev = j->prev;
j->prev->next = j->next;
job_list_reset(j);
return j;
}
void
job_list_insert(Job *head, Job *j)
{
if (!job_list_is_empty(j)) return; /* already in a linked list */
j->prev = head->prev;
j->next = head;
head->prev->next = j;
head->prev = j;
}
/* for unit tests */
size_t
get_all_jobs_used()
{
return all_jobs_used;
}
+86
View File
@@ -0,0 +1,86 @@
#define _XOPEN_SOURCE 600
#include "dat.h"
#include <unistd.h>
#include <sys/types.h>
#include <stdint.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/epoll.h>
#ifndef EPOLLRDHUP
#define EPOLLRDHUP 0x2000
#endif
static int epfd;
int
sockinit(void)
{
epfd = epoll_create(1);
if (epfd == -1) {
twarn("epoll_create");
return -1;
}
return 0;
}
int
sockwant(Socket *s, int rw)
{
int op;
if (!s->added && !rw) {
return 0;
} else if (!s->added && rw) {
s->added = 1;
op = EPOLL_CTL_ADD;
} else if (!rw) {
op = EPOLL_CTL_DEL;
} else {
op = EPOLL_CTL_MOD;
}
struct epoll_event ev = {.events=0};
switch (rw) {
case 'r':
ev.events = EPOLLIN;
break;
case 'w':
ev.events = EPOLLOUT;
break;
}
ev.events |= EPOLLRDHUP | EPOLLPRI;
ev.data.ptr = s;
return epoll_ctl(epfd, op, s->fd, &ev);
}
int
socknext(Socket **s, int64 timeout)
{
int r;
struct epoll_event ev = {.events=0};
r = epoll_wait(epfd, &ev, 1, (int)(timeout/1000000));
if (r == -1 && errno != EINTR) {
twarn("epoll_wait");
exit(1);
}
if (r > 0) {
*s = ev.data.ptr;
if (ev.events & (EPOLLHUP|EPOLLRDHUP)) {
return 'h';
} else if (ev.events & EPOLLIN) {
return 'r';
} else if (ev.events & EPOLLOUT) {
return 'w';
}
}
return 0;
}
+114
View File
@@ -0,0 +1,114 @@
#include "dat.h"
#include <stdint.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pwd.h>
#include <fcntl.h>
static void
su(const char *user)
{
errno = 0;
struct passwd *pwent = getpwnam(user);
if (errno) {
twarn("getpwnam(\"%s\")", user);
exit(32);
}
if (!pwent) {
twarnx("getpwnam(\"%s\"): no such user", user);
exit(33);
}
int r = setgid(pwent->pw_gid);
if (r == -1) {
twarn("setgid(%d \"%s\")", pwent->pw_gid, user);
exit(34);
}
r = setuid(pwent->pw_uid);
if (r == -1) {
twarn("setuid(%d \"%s\")", pwent->pw_uid, user);
exit(34);
}
}
static void
handle_sigterm_pid1()
{
exit(143);
}
static void
set_sig_handlers()
{
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sa.sa_flags = 0;
int r = sigemptyset(&sa.sa_mask);
if (r == -1) {
twarn("sigemptyset()");
exit(111);
}
r = sigaction(SIGPIPE, &sa, 0);
if (r == -1) {
twarn("sigaction(SIGPIPE)");
exit(111);
}
sa.sa_handler = enter_drain_mode;
r = sigaction(SIGUSR1, &sa, 0);
if (r == -1) {
twarn("sigaction(SIGUSR1)");
exit(111);
}
// Workaround for running the server with pid=1 in Docker.
// Handle SIGTERM so the server is killed immediately and
// not after 10 seconds timeout. See issue #527.
if (getpid() == 1) {
sa.sa_handler = handle_sigterm_pid1;
r = sigaction(SIGTERM, &sa, 0);
if (r == -1) {
twarn("sigaction(SIGTERM)");
exit(111);
}
}
}
int
main(int argc, char **argv)
{
UNUSED_PARAMETER(argc);
progname = argv[0];
setlinebuf(stdout);
optparse(&srv, argv+1);
if (verbose) {
printf("pid %d\n", getpid());
}
int r = make_server_socket(srv.addr, srv.port);
if (r == -1) {
twarnx("make_server_socket()");
exit(111);
}
srv.sock.fd = r;
prot_init();
if (srv.user)
su(srv.user);
set_sig_handlers();
srv_acquire_wal(&srv);
srvserve(&srv);
exit(0);
}
+110
View File
@@ -0,0 +1,110 @@
#include "dat.h"
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
void
ms_init(Ms *a, ms_event_fn oninsert, ms_event_fn onremove)
{
a->len = a->cap = a->last = 0;
a->items = NULL;
a->oninsert = oninsert;
a->onremove = onremove;
}
static int
grow(Ms *a)
{
void **nitems;
size_t ncap = a->cap << 1;
if (!ncap)
ncap = 1;
nitems = malloc(ncap * sizeof(void *));
if (!nitems)
return 0;
memcpy(nitems, a->items, a->len * sizeof(void *));
free(a->items);
a->items = nitems;
a->cap = ncap;
return 1;
}
int
ms_append(Ms *a, void *item)
{
if (a->len >= a->cap && !grow(a))
return 0;
a->items[a->len++] = item;
if (a->oninsert)
a->oninsert(a, item, a->len - 1);
return 1;
}
static int
ms_delete(Ms *a, size_t i)
{
void *item;
if (i >= a->len)
return 0;
item = a->items[i];
a->items[i] = a->items[--a->len];
/* it has already been removed now */
if (a->onremove)
a->onremove(a, item, i);
return 1;
}
void
ms_clear(Ms *a)
{
while (ms_delete(a, 0));
free(a->items);
ms_init(a, a->oninsert, a->onremove);
}
int
ms_remove(Ms *a, void *item)
{
size_t i;
for (i = 0; i < a->len; i++) {
if (a->items[i] == item)
return ms_delete(a, i);
}
return 0;
}
int
ms_contains(Ms *a, void *item)
{
size_t i;
for (i = 0; i < a->len; i++) {
if (a->items[i] == item)
return 1;
}
return 0;
}
void *
ms_take(Ms *a)
{
void *item;
if (!a->len)
return NULL;
// The result of last behaviour is that ms_take returns the oldest elements
// first, exception is a row of multiple take calls without inserts on ms
// of even number of elements. See the test.
a->last = a->last % a->len;
item = a->items[a->last];
ms_delete(a, a->last);
++a->last;
return item;
}
+261
View File
@@ -0,0 +1,261 @@
#include "dat.h"
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#ifdef HAVE_LIBSYSTEMD
#include <systemd/sd-daemon.h>
#endif
static int
set_nonblocking(int fd)
{
int flags, r;
flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) {
twarn("getting flags");
return -1;
}
r = fcntl(fd, F_SETFL, flags | O_NONBLOCK);
if (r == -1) {
twarn("setting O_NONBLOCK");
return -1;
}
return 0;
}
static int
make_inet_socket(char *host, char *port)
{
int fd = -1, flags, r;
struct linger linger = {0, 0};
struct addrinfo *airoot, *ai, hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
r = getaddrinfo(host, port, &hints, &airoot);
if (r != 0) {
twarnx("getaddrinfo(): %s", gai_strerror(r));
return -1;
}
for (ai = airoot; ai; ai = ai->ai_next) {
fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (fd == -1) {
twarn("socket()");
continue;
}
r = set_nonblocking(fd);
if (r == -1) {
close(fd);
continue;
}
flags = 1;
r = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof flags);
if (r == -1) {
twarn("setting SO_REUSEADDR on fd %d", fd);
close(fd);
continue;
}
r = setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &flags, sizeof flags);
if (r == -1) {
twarn("setting SO_KEEPALIVE on fd %d", fd);
close(fd);
continue;
}
r = setsockopt(fd, SOL_SOCKET, SO_LINGER, &linger, sizeof linger);
if (r == -1) {
twarn("setting SO_LINGER on fd %d", fd);
close(fd);
continue;
}
r = setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flags, sizeof flags);
if (r == -1) {
twarn("setting TCP_NODELAY on fd %d", fd);
close(fd);
continue;
}
if (host == NULL && ai->ai_family == AF_INET6) {
flags = 0;
r = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &flags, sizeof(flags));
if (r == -1) {
twarn("setting IPV6_V6ONLY on fd %d", fd);
close(fd);
continue;
}
}
r = bind(fd, ai->ai_addr, ai->ai_addrlen);
if (r == -1) {
twarn("bind()");
close(fd);
continue;
}
if (verbose) {
char hbuf[NI_MAXHOST], pbuf[NI_MAXSERV], *h = host, *p = port;
struct sockaddr_in addr;
socklen_t addrlen;
addrlen = sizeof(addr);
r = getsockname(fd, (struct sockaddr *) &addr, &addrlen);
if (!r) {
r = getnameinfo((struct sockaddr *) &addr, addrlen,
hbuf, sizeof(hbuf),
pbuf, sizeof(pbuf),
NI_NUMERICHOST|NI_NUMERICSERV);
if (!r) {
h = hbuf;
p = pbuf;
}
}
if (ai->ai_family == AF_INET6) {
printf("bind %d [%s]:%s\n", fd, h, p);
} else {
printf("bind %d %s:%s\n", fd, h, p);
}
}
r = listen(fd, 1024);
if (r == -1) {
twarn("listen()");
close(fd);
continue;
}
break;
}
freeaddrinfo(airoot);
if(ai == NULL)
fd = -1;
return fd;
}
static int
make_unix_socket(char *path)
{
int fd = -1, r;
struct stat st;
struct sockaddr_un addr;
const size_t maxlen = sizeof(addr.sun_path) - 1; // Reserve the last position for '\0'
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
if (strlen(path) > maxlen) {
warnx("socket path %s is too long (%ld characters), where maximum allowed is %ld",
path, strlen(path), maxlen);
return -1;
}
strncpy(addr.sun_path, path, maxlen);
r = stat(path, &st);
if (r == 0) {
if (S_ISSOCK(st.st_mode)) {
warnx("removing existing local socket to replace it");
r = unlink(path);
if (r == -1) {
twarn("unlink");
return -1;
}
} else {
twarnx("another file already exists in the given path");
return -1;
}
}
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd == -1) {
twarn("socket()");
return -1;
}
r = set_nonblocking(fd);
if (r == -1) {
close(fd);
return -1;
}
r = bind(fd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un));
if (r == -1) {
twarn("bind()");
close(fd);
return -1;
}
if (verbose) {
printf("bind %d %s\n", fd, path);
}
r = listen(fd, 1024);
if (r == -1) {
twarn("listen()");
close(fd);
return -1;
}
return fd;
}
int
make_server_socket(char *host, char *port)
{
#ifdef HAVE_LIBSYSTEMD
int fd = -1, r;
/* See if we got a listen fd from systemd. If so, all socket options etc
* are already set, so we check that the fd is a TCP or UNIX listen socket
* and return. */
r = sd_listen_fds(1);
if (r < 0) {
twarn("sd_listen_fds");
return -1;
}
if (r > 0) {
if (r > 1) {
twarnx("inherited more than one listen socket;"
" ignoring all but the first");
}
fd = SD_LISTEN_FDS_START;
r = sd_is_socket_inet(fd, 0, SOCK_STREAM, 1, 0);
if (r < 0) {
twarn("sd_is_socket_inet");
errno = -r;
return -1;
}
if (r == 0) {
r = sd_is_socket_unix(fd, SOCK_STREAM, 1, NULL, 0);
if (r < 0) {
twarn("sd_is_socket_unix");
errno = -r;
return -1;
}
if (r == 0) {
twarnx("inherited fd is not a TCP or UNIX listening socket");
return -1;
}
}
return fd;
}
#endif
if (host && !strncmp(host, "unix:", 5)) {
return make_unix_socket(&host[5]);
} else {
return make_inet_socket(host, port);
}
}
+8
View File
@@ -0,0 +1,8 @@
How to make a release:
1. Check out master
2. Tag the commit to release devX.Y: git tag dev1.11
3. Write a change log in file News (don't commit it)
4. Run pkg/dist.sh and follow the intructions.
5. Push tags and beanstalkd.github.io
6. Run pkg/mail.sh
+88
View File
@@ -0,0 +1,88 @@
%define beanstalkd_user beanstalkd
%define beanstalkd_group %{beanstalkd_user}
%define beanstalkd_home %{_localstatedir}/lib/beanstalkd
%define beanstalkd_logdir %{_localstatedir}/log/beanstalkd
Name: beanstalkd
Version: @VERSION@
Release: 0%{?dist}
Summary: A simple, fast workqueue service
Group: System Environment/Daemons
License: GPLv3+
URL: http://xph.us/software/%{name}/
Source0: http://xph.us/dist/%{name}/rel/%{name}-%{version}.tar.gz
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
Requires(pre): %{_sbindir}/useradd
Requires(post): /sbin/chkconfig
Requires(preun): /sbin/chkconfig, /sbin/service
Requires(postun): /sbin/service
%description
Beanstalk is a simple, fast workqueue service. Its interface is generic, but
was originally designed for reducing the latency of page views in high-volume
web applications by running time-consuming tasks asynchronously.
%prep
%setup -q
if [ ! -e configure ]; then
sh buildconf.sh
fi
%build
%configure --disable-rpath --docdir=%{_defaultdocdir}/%{name}-%{version}
make %{?_smp_mflags}
%install
rm -rf $RPM_BUILD_ROOT
make install DESTDIR=$RPM_BUILD_ROOT
%{__install} -p -D -m 0644 doc/%{name}.1 %{buildroot}%{_mandir}/man1/%{name}.1
%{__install} -p -D -m 0755 scripts/%{name}.init %{buildroot}%{_initrddir}/%{name}
%{__install} -p -D -m 0644 scripts/%{name}.sysconfig %{buildroot}%{_sysconfdir}/sysconfig/%{name}
%clean
rm -rf $RPM_BUILD_ROOT
%pre
%{_sbindir}/useradd -c "beanstalkd user" -s /bin/false -r -m -d %{beanstalkd_home} %{beanstalkd_user} 2>/dev/null || :
%post
/sbin/chkconfig --add %{name}
%preun
if [ $1 = 0 ]; then
/sbin/service %{name} stop >/dev/null 2>&1
/sbin/chkconfig --del %{name}
fi
%postun
if [ $1 -ge 1 ]; then
/sbin/service %{name} condrestart > /dev/null 2>&1 || :
fi
%files
%defattr(-,root,root,-)
%doc %{_defaultdocdir}/%{name}-%{version}/protocol.txt
%doc README COPYING doc/protocol.txt
%{_initrddir}/%{name}
%{_bindir}/%{name}
%{_mandir}/man1/%{name}.1.gz
%config(noreplace) %{_sysconfdir}/sysconfig/%{name}
%changelog
* Thu Oct 1 2009 Keith Rarick <kr@xph.us> - 1.4-0
- Convert this file to an autoconf template
- Tweak the summary and description
* Sun Jan 4 2009 Ask Bjørn Hansen <ask@develooper.com> - 1.2-0
- 1.2-tobe
- Use man page and .init/sysconfig scripts from .tar.gz
* Sat Nov 22 2008 Jeremy Hinegardner <jeremy at hinegardner dot org> - 1.1-1
- initial spec creation
+8
View File
@@ -0,0 +1,8 @@
---
layout: post
title: Beanstalkd @VERSION@ Release Notes
version: @VERSION@
dist: https://github.com/beanstalkd/beanstalkd/archive/v@VERSION@.tar.gz
file: beanstalkd-@VERSION@.tar.gz
---
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -e
set -o pipefail
exp() {
sed s/@VERSION@/$ver/ | sed s/@PARENT@/$prev/
}
clean() {
rm -f "$GIT_INDEX_FILE"
}
mkobj() {
git hash-object -w --stdin
}
die() {
echo >&2 "$@"
exit 2
}
ver=`./vers.sh`
case $ver in *+*) die bad ver $ver ;; esac
prev=`git describe --abbrev=0 --match=dev* --tags dev$ver^|sed s/^dev//`
test -n "$prev" || die no prev ver
test -f News || die no News
export GIT_INDEX_FILE
GIT_INDEX_FILE=`mktemp -t beanstalkd-dist-index-XXX`
trap clean EXIT
git read-tree dev$ver
newsobj=`cat News pkg/newstail.in|exp|mkobj`
versobj=`echo "printf '$ver'"|mkobj`
specobj=`exp <pkg/beanstalkd.spec.in|mkobj`
git update-index --add --cacheinfo 100644 $newsobj News
git update-index --cacheinfo 100755 $versobj vers.sh
git update-index --add --cacheinfo 100644 $specobj beanstalkd.spec
tree=`git write-tree`
commit=`git commit-tree $tree -p dev$ver -m "release $ver"`
git tag -m "beanstalkd version $ver" v$ver $commit
postcont=`(exp <pkg/bloghead.in; git cat-file blob v$ver:News)`
postfile=`date +%Y-%m-%d`-$ver-release-notes.md
echo "$postcont" >$postfile
echo "Now, run these commands to update the blog:"
echo "(mv $postfile ../beanstalkd.github.io/_posts/ && cd ../beanstalkd.github.io && git add . && git commit -m \"announce release $ver\")"
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
set -o pipefail
die() {
echo >&2 "$@"
exit 2
}
addr=beanstalk-talk@googlegroups.com
ver=`./vers.sh`
case $ver in *+*) die bad ver $ver ;; esac
(cat <<end; git cat-file -p v$ver:News)|msmtp -t $addr
To: $addr
From: `git config user.name` <`git config user.email`>
Subject: [ANN] beanstalkd $ver
end
+20
View File
@@ -0,0 +1,20 @@
Full list of changes (includes authorship information):
<http://github.com/beanstalkd/beanstalkd/compare/v@PARENT@...v@VERSION@>
Our Urls
--------
Download the @VERSION@ tarball directly:
<https://github.com/beanstalkd/beanstalkd/archive/v@VERSION@.tar.gz>
Learn all about beanstalk:
<https://beanstalkd.github.io/>
Talk about beanstalk development or use at:
<https://groups.google.com/group/beanstalk-talk>
Bugs
----
Please report any bugs to:
<http://github.com/beanstalkd/beanstalkd/issues>
+57
View File
@@ -0,0 +1,57 @@
#include <stdlib.h>
// prime // downscale treshold / upscale treshold
size_t primes[] = {
12289, // NA / 3072
24593, // 1537 / 6148
49193, // 3074 / 12298
98387, // 6149 / 24596
196799, // etc
393611,
787243,
1574491,
3148987,
6297979,
12595991,
25191989,
50383981,
100767977,
201535967,
403071937,
806143879,
1612287763,
3224575537UL,
#if _LP64
6449151103,
12898302233,
25796604473,
51593208973,
103186417951,
206372835917,
412745671837,
825491343683,
1650982687391,
3301965374803,
6603930749621,
13207861499251,
26415722998507,
52831445997037,
105662891994103,
211325783988211,
422651567976461,
845303135952931,
1690606271905871,
3381212543811743,
6762425087623523,
13524850175247127,
27049700350494287,
54099400700988593,
108198801401977301,
216397602803954641,
432795205607909293,
865590411215818597,
1731180822431637217,
#endif
};
+2339
View File
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
# Многопроцессный Beanstalkd
Статус: постановка на реализацию, код не реализован.
Дата фиксации: 2026-10-15.
База: исходники Beanstalkd 1.13 в этом каталоге.
## Цель
Позволить одному логическому серверу использовать несколько CPU-ядер для обработки независимых tubes, сохранив модель заданий Beanstalkd и динамический жизненный цикл очередей.
Принятое решение: процессы-владельцы групп tubes. У каждого владельца собственный event loop, задания, очереди, таймеры и WAL; сетевые frontend-процессы направляют операции владельцам через IPC. Supervisor обеспечивает эксклюзивность owner slots и управление жизненным циклом процессов.
```text
+-- owner slot 0: tubes, jobs, WAL
clients --> frontend(s) --IPC--+-- owner slot 1: tubes, jobs, WAL
+-- owner slot N: tubes, jobs, WAL
supervisor: topology, process generations, startup, shutdown, restart
```
## Границы Первого Этапа
- Один хост, статическая карта распределения, отдельный WAL каждого владельца.
- Каждый `reserve` / `reserve-with-timeout` работает с одной наблюдаемой tube. Настройка `watch target`, `ignore default` допускается.
- Связь tube с владельцем определяется по имени через фиксированные логические разделы. Постоянного каталога дескрипторов всех tubes нет.
- Consumer может держать несколько заданий. Его `use`, текущий watch list и ранее полученные резервации не обязаны относиться к одной tube или одному владельцу.
- Политика перехода от существующего однопроцессного сервера определяется в эпике 01. Рекомендуется opt-in с сохранением legacy, но обязательная поддержка двух режимов пока не утверждена.
- Полный end-to-end путь команд, восстановление, диагностика и проверки отказов входят в результат, а не откладываются после демонстрации `put`/`reserve`.
## Вне Объёма
- Межшардовый выбор задания из нескольких наблюдаемых tubes.
- Распараллеливание одной tube, глобальный FIFO между tubes.
- Динамическая миграция, автоматическая балансировка и изменение карты при работе сервера.
- Кластер между хостами, репликация и автоматическое назначение данных упавшего владельца другому slot.
- Shared-memory allocator, zero-copy и io_uring как предварительное условие реализации.
- Exactly-once выполнение пользовательского кода.
## Основные Инварианты
1. Имя tube всегда имеет одного владельца в пределах версии топологии, даже когда объект tube не существует.
2. Объект создаётся только владельцем и только по операции, которая предполагает создание по протоколу.
3. Два процесса не изменяют данные одного owner slot одновременно.
4. Переходы состояний job последовательны у владельца; права на reserved job принадлежат логической клиентской сессии.
5. Один reserve имеет не более одного результата; job имеет не более одной действующей резервации.
6. Очерёдность команд и ответов одного TCP-соединения сохраняется.
7. Сбой, недоступность и неизвестный результат не маскируются под успешную операцию или отсутствие задания.
8. Накопление IPC-запросов, ответов и тел ограничено памятью; медленный клиент не блокирует чужие очереди неограниченно.
9. Перезапуск с WAL использует ту же топологию и корректное пространство ID; несовпадение конфигурации обнаруживается до записи данных.
## Эпики И Зависимости
| ID | Постановка | Зависимости | Результат |
| --- | --- | --- | --- |
| 01 | [Контракты и baseline](01-contract.md) | Нет | Решения по внешнему поведению, отказам, ID и конфигурации |
| 02 | [Владение tubes](02-ownership.md) | 01 | Детерминированная маршрутизация и локальное создание |
| 03 | [IPC и сессии](03-ipc-sessions.md) | 01; интерфейсы 02 | Межпроцессный транспорт и безопасный жизненный цикл сессий |
| 04 | [Queue engine и протокол](04-queue-engine.md) | 02, 03 | Все операции над очередями через владельцев |
| 05 | [WAL и ID](05-persistence.md) | 01, 02; интеграция 03, 04 | Восстановление, уникальные ID, изоляция журналов |
| 06 | [Supervisor и эксплуатация](06-supervisor.md) | 02, 03; интеграция 04, 05 | Управляемый запуск, отказ и остановка процессов |
| 07 | [Приёмка и производительность](07-validation.md) | Baseline после 01; итог после 02-06 | Воспроизводимые функциональные и нагрузочные доказательства |
Порядок не запрещает совместную разработку согласованных интерфейсов. Минимальный запуск дочернего процесса для тестов можно сделать раньше эпика 06; это не означает готовность supervisor. Тесты пишутся в каждом эпике, а не только в конце.
## Общая Приёмка
- Все обязательные решения эпика 01 закрыты и документированы до реализации зависящего от них контракта.
- Исходные тесты используются как регрессионная база; отклонения допустимы только по согласованной матрице совместимости. Если выбран отдельный legacy-режим, он проходит прежний набор без ограничения multi-watch.
- Успешно проверены минимум два owner slots и несколько frontend-процессов, в том числе гонка создания одной tube через разные frontend.
- Протестированы job lifecycle, права сессий, таймеры, slow clients, аварии и WAL при одинаковых с baseline настройках durability.
- На независимых tubes подтверждён рост пропускной способности и реальная работа нескольких owner CPU; точный порог фиксируется после baseline, до финальных замеров.
- Для одной горячей tube ограничение последовательности явно показано в отчёте, а не скрыто агрегированной нагрузкой.
- Обновлены `README.md`, `doc/protocol.txt`, исходник руководства и эксплуатационные инструкции при реализации. Внешние ограничения и совместимость старых данных описаны явно.
## Основание
Текущий сервер последовательно выполняет `prottick`, `socknext` и callback в `serv.c:srvserve`. Реестры jobs/tubes и генератор ID глобальны. `process_queue` связывает job с `Conn*`, а WAL защищён от второго владельца. Поэтому разделение только listening socket или вызов `fork()` не дают корректного общего сервера.
Критический краткий контекст для последующей работы: [../AGENTS.md](../AGENTS.md).
+52
View File
@@ -0,0 +1,52 @@
# 01. Контракты И Baseline
Статус: не начат. Зависимости: нет.
## Цель
До изменения модели исполнения зафиксировать проверяемые границы совместимости, интерфейсы процессов и условия сравнения производительности. Принятое архитектурное направление не является утверждением всех деталей ниже.
## Задачи
1. Сопоставить `doc/protocol.txt` с `prot.c`, `conn.c`, `job.c`, `tube.c` и WAL. Зафиксировать значимые расхождения кода и документации, не исправляя их попутно без отдельного решения.
2. Описать роли supervisor, frontend, owner; определить число frontend и способ принятия соединений. Не создавать центрального диспетчера всех queue-операций без измерения его предела.
3. Определить opt-in нового режима, конфигурацию топологии и совместимость со старым запуском, TCP/Unix sockets, socket activation, сменой пользователя и сигналами.
4. Снять baseline существующей версии: CPU, throughput, latency, память и WAL I/O. Сохранить воспроизводимые команды и параметры.
5. Подготовить таблицу состояний сессии, ожидания reserve, передачи ответа, аварии frontend и owner. Указать точки изменения состояния и подтверждения операции.
## Решения До Реализации
| Вопрос | Ограничение для решения | Где реализуется |
| --- | --- | --- |
| Переход и legacy | Выбрать сохранение отдельного legacy с opt-in либо согласованный переход на новую архитектуру. Два режима не являются уже утверждённым обязательством; новый режим с одним owner не равен прежнему серверу по возможностям | 04, 05, 06, 07 |
| Multi-watch reserve | Не выбирать первую tube и не выдавать `TIMED_OUT` как будто очередь пуста. Выбрать документированный отказ/закрытие или явно согласованное расширение; не объявлять такой ответ стандартом 1.13 | 04 |
| Наблюдение одной tube, резервации нескольких | Сохранить `reserve-job`, смену watch list и проверки TTR/`DEADLINE_SOON` по всем резервациям сессии; дополнительное ограничение требует отдельного согласования | 03, 04 |
| Неизвестный результат мутации | Не повторять `put` автоматически. Определить закрытие соединения и судьбу сессии при разрыве IPC; request ID не даёт durable exactly-once | 03, 06 |
| Сбой владельца | Выбрать политику затронутых TCP-сессий; безопасная исходная стратегия: закрыть затронутые сессии и очистить их ссылки у остальных владельцев. Не продолжать старые резервации после replay как будто аварии не было | 03, 05, 06 |
| Формат топологии | Фиксировать hash/version/seed при наличии, число разделов, карту, slot IDs, fingerprint. Проверять до открытия WAL для записи | 02, 05 |
| Job ID | Decimal uint64 на wire; уникальность, маршрутизация, порядок внутри tube, переполнение и частичные перезапуски. Определить импорт старых ID | 04, 05 |
| Старый WAL | Выбрать явный offline import либо контролируемый отказ нового режима; исходные данные не изменять автоматически. Если legacy сохраняется, он должен читать прежние данные | 05 |
| Диагностика | Определить смысл `pid`, `id`, `uptime`, `rusage-*`, binlog-полей и счётчиков после частичного перезапуска | 04, 06 |
| Платформы | Определить поддерживаемые платформы новой архитектуры и политику остальных; если legacy сохраняется, его существующие платформы не регрессируют. Linux-first допустим только как явная граница | 03, 06, 07 |
| Численные лимиты | Утвердить лимиты IPC/body buffering, сроки очистки умерших сессий, restart policy и критерии производительности | 03, 06, 07 |
Открытые решения оформлять в соответствующем документе как выбранный контракт с обоснованием. Внешние ограничения сверх одной наблюдаемой tube выносить на согласование, а не выбирать неявно в коде.
## Baseline
- Матрица: одна/много tubes, один/много клиентов, короткие/крупные bodies, ready/delayed workload, WAL off/on, обычный fsync и `-f 0`.
- Генератор нагрузки не должен насыщать CPU раньше сервера. Фиксировать CPU affinity, оборудование, ОС, compiler/CFLAGS, размеры данных и семя распределения.
- Измерять завершённый lifecycle `put -> reserve -> delete` и отдельные операции; точно определить единицу throughput.
- Выполнить профилирование: обход tubes в `next_awaited_job` и `soonest_delayed_job`, системные вызовы, копирование, WAL. Не считать кандидатов доказанными bottlenecks без профиля.
- Не менять алгоритмы исходной версии в рамках снятия baseline.
## Приёмка
- Для каждой строки таблицы есть выбранное поведение либо явно блокируемая ею реализация; блокирующие решения закрыты до начала зависимых изменений.
- Сценарии transient multi-watch, смены tube с живой резервацией и `reserve-job` другой tube присутствуют в тест-плане.
- Есть спецификация запуска, топологии, session identity, command ownership и ошибок.
- Есть сохранённый baseline и заранее заданные критерии сравнения для эпика 07.
## Точки Кода
`main.c`, `util.c:optparse`, `net.c`, `serv.c`, `prot.c:dispatch_cmd`, `conn.c`, `dat.h`, `Makefile`, `testserv.c`.
+56
View File
@@ -0,0 +1,56 @@
# 02. Владение Динамическими Tubes
Статус: не начат. Зависимости: 01.
## Цель
Однозначно назначать процесс всем возможным именам tubes без постоянного реестра самих очередей и без конкурентного claim при каждом создании.
## Контракт
```text
partition = stable_hash(validated_tube_name) % PARTITION_COUNT
owner_slot = partition_map[partition]
```
- Hash работает с точными байтами имени по протоколу: без изменения регистра, locale-зависимой обработки и рандомизации между процессами.
- `PARTITION_COUNT` фиксирован и не равен числу живых процессов по определению. Число разделов, алгоритм и карта выбираются в 01.
- Owner slot не является PID. Перезапуск процесса не меняет принадлежность имён.
- Все процессы подтверждают одинаковые fingerprint/version топологии до обслуживания запросов.
- Данные с несовместимой картой не открываются для записи. Отсутствующая или повреждённая метаинформация не должна вызывать создание новой карты поверх существующих данных.
- Без WAL карта всё равно должна быть единой для живого логического сервера. При полном запуске без сохранённых данных допустима новая явно заданная конфигурация.
## Задачи
1. Реализовать проверку и загрузку карты разделов, стабильный router и тестовые векторы hash.
2. Отделить разрешение владельца от локального поиска/создания объекта. `find` не имеет побочного эффекта `create`.
3. Оставить `tube_find_or_make` или его минимально изменённый эквивалент внутри владельца. Реестр локальных tubes содержит только его объекты.
4. Обрабатывать `use`, `watch`, `ignore` и disconnect через ссылки логических сессий. Подтверждать изменение клиентского состояния после корректного изменения ссылок; предусмотреть rollback частичного attach.
5. Сохранять tube пока есть job любого живого состояния или удерживающая клиентская/системная ссылка. Удалять после освобождения последней ссылки без изменения карты.
6. Определить `default`: одна логическая tube на всём сервере у вычисленного владельца, не отдельная пользовательская очередь в каждом процессе. Сохранить штатную системную ссылку, если она нужна для эквивалентности текущему `prot_init`.
7. При WAL replay создавать tubes у соответствующего владельца; чужое имя в WAL должно выявлять неверную топологию/раскладку данных, а не тихо создавать вторую tube.
## Сценарий Одновременного Создания
Два frontend получают `use emails` и `watch emails`. Оба вычисляют один slot. Его event loop обрабатывает сообщения последовательно: первое создаёт tube, второе находит ту же tube и добавляет свою ссылку. Дополнительная distributed lock или таблица claims на имя не нужна.
Эксклюзивный claim требуется для самого owner slot и обеспечивается supervisor из эпика 06.
## Приёмка
- Hash-векторы одинаковы между процессами и перезапусками; тесты включают допустимые граничные имена, регистр и максимальную длину.
- Параллельные `use`/`watch` одной новой tube через разные frontend создают один объект у одного владельца и корректные счётчики ссылок.
- `stats-tube`/`pause-tube` несуществующего имени возвращают `NOT_FOUND` и не оставляют объект.
- Tube с reserved job не удаляется после ухода остальных клиентов.
- Пустая обычная tube удаляется после последней ссылки; повторное создание попадает в тот же slot.
- `default` представлен один раз в `list-tubes`; подключения не создают локальные копии в остальных owners.
- Авария frontend освобождает его ссылки; запоздалый attach старого поколения не воскрешает сессию.
- Подмена hash, карты или числа разделов при существующих данных приводит к безопасному отказу запуска, а не перемаршрутизации.
## Не Делать
Никакого `hash(name) % live_owners`, переноса очередей при сбое, автоматического распределения новых tubes по текущей нагрузке или долговечного каталога пустых tubes.
## Точки Кода
`tube.c`, `conn.c:on_watch/on_ignore`, `prot.c:prot_init/dispatch_cmd/prot_replay`, `dat.h:Tube`, новый минимальный модуль топологии при необходимости.
+45
View File
@@ -0,0 +1,45 @@
# 03. IPC И Логические Сессии
Статус: не начат. Зависимости: 01, интерфейсы маршрутизации 02.
## Цель
Отделить TCP-соединение от состояния consumer у владельцев. Обеспечить межпроцессный обмен без общих указателей, неограниченных буферов и неявного повторения команд.
## Задачи
1. Реализовать неблокирующий IPC на Unix domain sockets с явным framing; выбрать stream или message-oriented транспорт с учётом крупных bodies и платформ.
2. Определить versioned envelope: topology fingerprint, поколения endpoints, session ID, request ID, opcode, размеры метаданных и body. Не сериализовать C-структуры с padding/указателями как wire contract.
3. Ввести session identity, уникальную в пределах поколения frontend, и идентичность запуска supervisor. Защититься от повторного использования PID/FD и запоздалых сообщений.
4. Хранить у frontend транспортное состояние, `use`, watch list и маршруты резерваций. Хранить у владельцев логические ссылки, ожидания и принадлежащие сессиям jobs.
5. Сохранить порядок команд и ответов на одном клиентском соединении, включая pipelined input. Не считать IPC request ID разрешением менять порядок протокола.
6. Ввести лимиты на размеры frames, входящие запросы, ожидающие ответы и body buffering. Превышение лимитов вызывает backpressure/задокументированный отказ, а не переполнение памяти.
7. Определить отмену reserve и detach сессии при disconnect, half-close, IPC failure и смерти frontend. Повторный detach должен быть безопасен; старый attach не должен отменять cleanup.
8. Определить управление памятью ответа: тело job, передаваемое медленному клиенту, остаётся доступным после timeout/delete исходного job. Копия или явная pin/reference имеют ограниченный срок жизни.
9. Не повторять мутации после неизвестного результата. Если предусмотрены повторы в живом канале, обеспечить дедупликацию в ограниченном окне с определённым моментом освобождения записей; не обещать сохранение dedup после crash без отдельного журнала.
## Межвладельческие Резервации Одной Сессии
Ограничение одной наблюдаемой tube не исключает сохранённые резервации у нескольких владельцев. Это возникает после смены watch list или `reserve-job`.
- Owner остаётся источником истины о job и его TTR.
- При ожидании нового reserve необходимо учесть ближайший deadline всех резерваций сессии, а не только текущей tube.
- Спроектировать обновления после `touch`, release/delete/bury, timeout и новой резервации, чтобы frontend/coordinator не принимал решения по устаревшему deadline.
- Описать упорядочивание событий job available, timeout, `DEADLINE_SOON`, disconnect. Только один из конкурирующих результатов завершает ожидающий запрос.
- Согласовать cleanup остальных владельцев при гибели одного owner и закрытии затронутой клиентской сессии.
Это координация состояния одной сессии, а не межшардовый выбор минимального priority из нескольких watch tubes. Смешивать эти задачи нельзя.
## Приёмка
- Тесты partial read/write, объединённых frames, некорректных длин, oversized body и разрыва IPC посреди сообщения проходят без повреждения состояния.
- Старые поколения, неизвестные session IDs и несоответствующая топология отклоняются до мутации.
- Медленный client/frontend не вызывает неограниченного роста памяти или блокировки всех owners.
- Disconnect во время `put`, ожидания reserve, выдачи job и передачи большого body корректно освобождает ресурсы.
- Авария frontend очищает все его сессии и резервации за установленное в 01 время; явно отличать завершившийся процесс от временно медленного.
- Потеря ответа на `put` не вызывает скрытого второго `put`; неоднозначный результат виден как ошибка соединения по контракту.
- Переключение tube при живой резервации, `reserve-job` другой tube и последующие `touch`/`DEADLINE_SOON` проверены через разные owners.
## Точки Кода
`dat.h:Conn/Socket/Job`, `conn.c`, `prot.c:conn_process_io/conn_timeout/reply_job`, платформенный event loop. Новый IPC-модуль должен иметь самостоятельные тесты framing и жизненного цикла.
+61
View File
@@ -0,0 +1,61 @@
# 04. Queue Engine И Протокол
Статус: не начат. Зависимости: 02, 03; ID/WAL интегрируются с 05.
## Цель
Все операции над заданиями исполняются владельцем соответствующей tube; frontend сохраняет протокольный контракт без общей памяти и центрального глобального планировщика.
## Задачи
1. Отделить parsing/response I/O от мутаций queue engine в `prot.c`. Сохранить существующие heaps, списки и однотредовое исполнение владельца, не переписывая их на shared memory.
2. Удалить зависимость владения job от локального `Conn*` frontend. Использовать логическую сессию и локальное owner-side состояние.
3. Сделать глобальные реестры и счётчики локальными роли процесса. Один процесс-владелец может сохранить process-local globals, если они не смешиваются с frontend; не вводить контейнеры/абстракции без необходимости.
4. Перенести delayed jobs, pause и TTR в timer loop владельца. Согласовать reserve timeout и safety margin с управлением сессиями из 03.
5. Реализовать маршрутизацию всех команд по таблице; подтвердить завершение каждой команды только в согласованной точке состояния/WAL.
6. Сохранить проверки числовых диапазонов, framing body/CRLF, ошибки нехватки памяти и порядок ответов.
7. Реализовать явно выбранную реакцию на multi-watch reserve. Не запретить промежуточный `watch target` при подключении с `default`.
## Маршрутизация
| Команды | Источник владельца/исполнитель |
| --- | --- |
| `use`, `watch`, `ignore` | Имена tubes и изменение ссылок сессии |
| `put`, `peek-ready`, `peek-delayed`, `peek-buried`, `kick` | Текущая `use` tube |
| `reserve`, `reserve-with-timeout` | Единственная наблюдаемая tube плюс состояние deadlines сессии |
| `peek`, `delete`, `release`, `bury`, `touch`, `reserve-job`, `kick-job`, `stats-job` | Job ID; не текущая `use`/watch tube |
| `pause-tube`, `stats-tube` | Явно указанное имя |
| `list-tube-used`, `list-tubes-watched` | Подтверждённое состояние сессии frontend |
| `list-tubes`, `stats` | Сбор данных владельцев/frontend по определённому контракту |
| `quit` | Закрытие клиентской сессии и cleanup у всех затронутых owners |
## Совместимость
- Priority/FIFO сохраняются внутри tube. Текущий `job_pri_less` использует `(priority, id)`; изменение ID требует доказательства эквивалентности или отдельного ordering field.
- `reserve-job` не привязывается к watch list и сохраняет выбор ready/buried/delayed job по ID.
- `delete` чужой reserved job недопустим, но ready/delayed/buried jobs доступны по прежним правилам. `release`, `bury`, `touch` проверяют сессию-владельца резервации.
- Disconnect возвращает резервации в очереди; timeout во время передачи body не приводит к use-after-free.
- `use` не меняет watch list, а `ignore` не освобождает уже зарезервированные jobs.
- Недоступный owner не трактуется как пустая tube или отсутствующий job. Ответы `NOT_FOUND`/`TIMED_OUT` не подменяют отказ инфраструктуры.
## Статистика
- Суммировать только действительно аддитивные job/command counters с определённым владельцем каждого счётчика.
- Connection/producer/worker/waiting counters считают уникальные клиентские сессии, а не IPC endpoints и не число owner-side копий.
- `list-tubes` объединяет реальные объекты, не все возможные имена, и не дублирует `default`.
- Определить согласованность snapshot при конкурентных изменениях. Не обещать атомарный глобальный snapshot без реализации барьера.
- Для `pid`, server `id`, `uptime`, `rusage-*` и binlog-полей применять контракт 01. Номера сегментов разных WAL нельзя бессмысленно суммировать.
## Приёмка
- Каждая строка routing table имеет успешные, ошибочные и межвладельческие тесты, где применимо.
- Полный lifecycle ready/reserved/delayed/buried и приоритеты проходят на двух и более owners.
- `use` одной tube при watch другой корректен, в том числе если owners различны.
- Один consumer может держать несколько jobs; смена watch/`reserve-job` не теряет старые резервации.
- Нулевая/положительная задержка reserve, safety margin, `touch`, pause/unpause и disconnect соответствуют выбранному контракту.
- Гонки reserve vs delete, timeout vs touch, pause vs reserve и disconnect vs reply не дают двух активных резерваций или двух ответов.
- Исходный `make check` используется по матрице совместимости 01. Если выбран отдельный legacy-режим, он проходит весь прежний набор, включая multi-tube; ограничение нового режима не переносится на legacy.
## Точки Кода
`prot.c`, `conn.c`, `job.c`, `tube.c`, `dat.h`, `testserv.c`, `testjobs.c`, `testheap.c`.
+44
View File
@@ -0,0 +1,44 @@
# 05. WAL, Job ID И Восстановление
Статус: не начат. Зависимости: 01, 02; интеграция 03, 04.
## Цель
Изолировать журналы владельцев и обеспечить восстановление без пересечения ID, появления двух копий tube или незаметной потери совместимости данных.
## Задачи
1. Определить layout общего data root, метаданных топологии и отдельных каталогов owner slots. Каждый WAL имеет одного writer и собственную блокировку.
2. Проверять топологию и layout до любых изменяющих операций с журналами; создавать начальные метаданные атомарно с необходимой durability, если сервер запускается с пустыми данными.
3. Задать формат decimal uint64 job ID и маршрутизацию к владельцу без глобального mutable каталога каждого job на критическом пути, если выбранная схема это позволяет.
4. Решить восстановление генератора ID при частичном restart: чтения максимального ID среди живых jobs недостаточно, если большие ID уже удалены. Учитывать старые сообщения/сессии и generation fencing; документировать срок гарантий уникальности.
5. Определить переполнение и исчерпание диапазона ID: контролируемый отказ вместо wraparound и коллизий.
6. Сохранить порядок FIFO внутри tube при изменении ID. Если вводится отдельный sequence, определить момент выдачи и запись/восстановление; не изменять порядок поступления относительно текущей версии без решения.
7. Запускать replay каждого owner отдельно. До готовности replay его partition space недоступен; frontend не перенаправляет операции к соседям.
8. Восстанавливать jobs в соответствии с текущей семантикой WAL, не считать клиентские резервации пережившими restart. Старые frontend-сессии не продолжают работу поверх восстановленных jobs.
9. Проверить compaction, segment rotation, file references, disk-full/short write/fsync errors и взаимодействие с подтверждением команд.
10. Реализовать выбранную в 01 политику старого WAL: отдельный offline import с сохранением оригинала либо явный отказ нового режима. Если отдельный legacy сохраняется, он читает прежний формат.
## Ограничения
- Нельзя совместно писать независимыми owners в существующий binlog-каталог, даже если убрать `waldirlock`.
- Tube descriptor не становится персистентным объектом: имя восстанавливается из живых jobs. Пустые клиентские tubes не обязаны переживать restart.
- Частичный перезапуск без WAL теряет данные только соответствующего owner по заявленному контракту; это нельзя выдавать за восстановление или HA.
- С `-b` границы потери данных определяются реальной записью и настройками fsync. Не требовать отсутствия любых потерь при `-F` и не ослаблять `-f 0` под видом ускорения.
- При изменении `Jobrec` следовать процедуре `Walver` в `dat.h`, включая чтение старых версий. Многопроцессность сама по себе не требует изменения `Jobrec`.
- Онлайн-миграция и изменение карты не входят в эпик. Увеличение числа slots с переназначением partitions требует отдельной offline-процедуры, не простого изменения config.
## Приёмка
- Два owners одновременно записывают задания, используют разные WAL и не пересекающиеся ID.
- После удаления всех jobs и частичного restart генерация ID не создаёт опасных коллизий по утверждённому контракту; тест включает запоздалые сообщения старого поколения.
- Неизменная топология восстанавливает jobs в правильных tubes и owners; несовместимая карта не меняет существующие файлы при отказе запуска.
- Полный/частичный restart проверен с ready, delayed, buried и ранее reserved jobs; внешние сессии не сохраняют ложное владение восстановленными jobs.
- Ошибка одного WAL не повреждает файлы другого. Реакция логического сервера соответствует 01/06.
- Проверены crash windows до/после записи и подтверждения, compaction, rotation, недостаток места и повреждённый журнал.
- Старые данные остаются пригодны для legacy либо импортируются явным инструментом согласно выбранной политике; исходник не уничтожается автоматически.
- Документированы ID layout, topology metadata, срок гарантий ID, WAL compatibility и ограничения durability.
## Точки Кода
`job.c:make_job_with_id/next_id/job_pri_less`, `dat.h:Jobrec/Walver/Wal/File`, `walg.c`, `file.c`, `serv.c:srv_acquire_wal`, `prot.c:prot_replay`, WAL-тесты в `testserv.c`.
+47
View File
@@ -0,0 +1,47 @@
# 06. Supervisor И Эксплуатация
Статус: не начат. Зависимости: 02, 03; итоговая интеграция 04, 05.
## Цель
Сделать многопроцессный сервер управляемым сервисом с единой конфигурацией, предсказуемым запуском и корректной реакцией на сбои.
## Задачи
1. Ввести supervisor, запускающий owners/frontend с явно заданными ролями, slot IDs, generations и топологией.
2. Обеспечить один активный процесс на slot. Перед заменой подтвердить завершение прежнего процесса; PID reuse не является подтверждением его идентичности.
3. Организовать startup barrier: проверка config, locks и топологии, восстановление WAL, IPC handshake, готовность к клиентским операциям.
4. Корректно закрывать лишние унаследованные listening/client/IPC/WAL descriptors. Не наследовать живые jobs/сессии через fork как способ совместного владения данными.
5. Интегрировать выбранные CLI/config, `-u`, TCP/Unix sockets и socket activation. Проверить границы привилегий и права на IPC/data root.
6. Реализовать согласованную обработку `SIGUSR1` drain и остановки. Drain прекращает новые `put` во всём логическом сервере, но не означает остановку выдачи существующих jobs.
7. Ввести restart policy с ограничением частоты/числа попыток и диагностикой. Во время недоступности slot его имена не передаются другим owners.
8. Определить поведение при смерти supervisor: оставшиеся процессы не продолжают принимать изменения без действующей управляющей эпохи; исключить запуск второго независимого сервера поверх тех же slots.
9. Обеспечить логи с role/slot/generation и метрики IPC backlog, restart count, owner readiness, загрузки и WAL ошибок.
10. Обновить эксплуатационную документацию: запуск, проверки готовности, restart, drain, резервное копирование/восстановление при остановленном сервере, запрет несогласованного изменения карты.
## Матрица Отказов
| Событие | Требуемое свойство |
| --- | --- |
| Смерть frontend | Владельцы освобождают ссылки/ожидания/резервации всех его сессий; старое поколение больше не принимается |
| Смерть owner | Затронутые сессии обрабатываются по контракту 01; очистка связанных сессий у остальных owners; перезапуск только того же slot |
| Разрыв IPC | Неизвестные результаты не повторяются автоматически; endpoint/session fencing исключает поздние изменения |
| Смерть supervisor | Нет бесконтрольных orphan writers и двух поколений активного сервера |
| Ошибка WAL | Явная диагностика и согласованная политика отказа; не тихое переключение на другой owner |
| `SIGUSR1` | Глобально согласованный drain, включая owners/frontend, перезапущенные во время drain |
| Остановка | Нет новых принятых операций после согласованного барьера; bounded shutdown, явная политика незавершённых ответов |
## Приёмка
- Запускаются один и несколько owners/frontend с одинаковой картой; ошибочная конфигурация не оставляет работающих дочерних процессов и занятых sockets.
- Попытка второго владельца slot отвергается как с WAL, так и без WAL.
- Kill/restart владельца сохраняет маршруты остальных tubes и не создаёт дубль упавшего slot.
- Затронутые клиенты получают документированный отказ/закрытие, а не `NOT_FOUND` из-за отсутствующего процесса.
- После перезапуска во время drain новый процесс не принимает `put` до снятия/завершения drain по контракту.
- Проверены supervisor/frontend/owner crashes, частичный startup failure, сигналы и отсутствие утечек FD/zombie-процессов.
- Запуск и платформы соответствуют выбранной в 01 политике перехода. Если legacy сохраняется, его startup и поддерживаемые платформы не регрессируют; неподдерживаемая платформа нового режима выдаёт явный отказ сборки/запуска.
- Обновлены руководство и примеры запуска; статусы/метрики позволяют определить недоступный slot без анализа PID по догадкам.
## Точки Кода
`main.c`, `serv.c`, `net.c`, `util.c`, платформенные event backends, `Makefile`, `doc/beanstalkd.ronn`, `README.md`, `testserv.c`.
+60
View File
@@ -0,0 +1,60 @@
# 07. Приёмка И Производительность
Статус: не начат. Baseline зависит от 01; итоговая приёмка зависит от 02-06.
## Цель
Доказать корректность и полезность многопроцессного режима воспроизводимыми тестами. Наличие нескольких PID или рост общей загрузки CPU сами по себе не являются приёмкой.
## Функциональная Матрица
| Область | Обязательные сценарии |
| --- | --- |
| Регрессии | `make`, `make check` по матрице 01; полный прежний набор при сохранении legacy; новый режим с 1 и несколькими owners |
| Создание tube | Одновременные `use`/`watch` через разные frontend, delete/recreate, единственный `default`, отсутствие создания на `stats-tube` |
| Routing | Все команды из 04; `use` и watch на разных owners; операции по ID независимо от текущей tube |
| Lifecycle | Ready/reserved/delayed/buried, kick/kick-job, приоритеты и FIFO, несколько резерваций на сессию |
| Watch | `watch target` перед `ignore default`; отказ multi-watch reserve; смена tube с живыми резервациями |
| Сессии | `reserve-job` другого owner, touch/delete старых резерваций, чужой reserved job, cleanup всех owners |
| Таймеры | Reserve timeout 0/positive, TTR, `DEADLINE_SOON`, delay, pause/unpause; гонки на границах времени |
| I/O | Partial requests/replies, pipelining, half-close, медленный client, large body, broken IPC, некорректные frame lengths |
| Ресурсы | IPC backpressure, OOM, ограниченные buffers, утечки памяти/FD после многократного reconnect |
| Отказы | Kill frontend/owner/supervisor, restart generations, stale messages, duplicate requests при наличии retry |
| WAL | Раздельные logs, partial/full restart, удалённые максимальные ID, compaction, rotation, disk full, crash windows |
| Эксплуатация | Drain, startup failure, невозможность двух writers, несовместимая карта, старые данные |
| Статистика | Нет двойного счёта сессий, один `default`, корректные process/binlog поля, недоступный owner |
## Свойства Для Проверки
- Один job не имеет двух действующих резерваций, но обработка внешним worker может повториться после TTR. Не объявлять корректные TTR redeliveries дубликатами сервера.
- Один ожидающий reserve завершается ровно одним исходом либо закрытием соединения; поздний IPC-ответ не меняет этот исход.
- Зафиксированный набор полностью завершённых операций согласуется с итоговым состоянием при quiescent проверке. Не требовать атомарности произвольного распределённого `stats` snapshot.
- После завершённого cleanup не остаются ссылки умершей сессии; сообщение старого поколения не создаёт их заново.
- Успешные операции не теряются сверх заявленных гарантий WAL/fsync. Неизвестный результат при обрыве соединения учитывается отдельно от подтверждённого успеха.
- Недопустимая топология отвергается без модификации исходных данных.
## Нагрузочная Матрица
1. Сравнить исходную версию, новый режим с одним owner, новый режим с 2/4/доступным числом owners и независимые исходные экземпляры как ориентир стоимости IPC.
2. Выполнить равномерную нагрузку по многим tubes, одну горячую tube и перекошенное распределение. Отдельно измерить создание/удаление множества короткоживущих tubes.
3. Варировать число frontend и клиентов; показать, не стал ли frontend/supervisor/генератор нагрузки новым bottleneck.
4. Повторить сравнения для WAL off/on, одинаковых `-f`/`-F`, размера body, queue depth и смеси команд.
5. Измерять throughput завершённых lifecycle и отдельных команд, p50/p95/p99 latency, CPU каждого процесса, RSS/суммарную память, IPC traffic/backlog, disk throughput/fsync latency.
6. Использовать прогрев, несколько повторов, одинаковые параметры сборки и инфраструктуры. Публиковать разброс и сырые результаты, а не только лучший запуск.
7. Сопоставить с численными критериями, зафиксированными в 01 после baseline и до итоговых тестов. Если пороги не выполнены, представить профиль и причину; эпик не считать пройденным автоматически.
## Средства
- Существующие команды: `make`, `make check`, `make bench`. Последняя является набором микробенчмарков, а не end-to-end испытанием.
- Добавить Unix integration harness для запуска ролей, kill/restart и инъекции отказов; переиспользовать `ct/` и существующие подходы `testserv.c`, где это сохраняет ясность.
- Для ошибок памяти использовать доступные sanitizers или эквивалентные средства на поддерживаемой платформе. Они не заменяют проверки межпроцессной семантики.
- Не завязывать таймерные тесты только на короткие sleeps: использовать синхронизацию/наблюдаемые события и разумные временные допуски, чтобы избежать flaky результатов.
## Итоговые Артефакты
- Автоматические тесты и воспроизводимые команды их запуска.
- Отчёт по compatibility matrix с явными ограничениями нового режима и политикой старых данных.
- Отчёт по производительности с baseline, конфигурациями, численными результатами и выводом о bottlenecks.
- Проверенные эксплуатационные инструкции и перечень оставшихся ограничений: одна tube последовательна, cross-shard watch reserve и live migration отсутствуют.
Реализация считается завершённой только после выполнения общей приёмки из [00-overview.md](00-overview.md), а не после успешного happy path на двух процессах.
+80
View File
@@ -0,0 +1,80 @@
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <sys/socket.h>
struct Server srv = {
.port = Portdef,
.wal = {
.filesize = Filesizedef,
.wantsync = 1,
.syncrate = DEFAULT_FSYNC_MS * 1000000,
},
};
// srv_acquire_wal tries to lock the wal dir specified by s->wal and
// replay entries from it to initialize the s state with jobs.
// On errors it exits from the program.
void srv_acquire_wal(Server *s) {
if (s->wal.use) {
// We want to make sure that only one beanstalkd tries
// to use the wal directory at a time. So acquire a lock
// now and never release it.
if (!waldirlock(&s->wal)) {
twarnx("failed to lock wal dir %s", s->wal.dir);
exit(10);
}
Job list = {.prev=NULL, .next=NULL};
list.prev = list.next = &list;
walinit(&s->wal, &list);
int ok = prot_replay(s, &list);
if (!ok) {
twarnx("failed to replay log");
exit(1);
}
}
}
void
srvserve(Server *s)
{
Socket *sock;
if (sockinit() == -1) {
twarnx("sockinit");
exit(1);
}
s->sock.x = s;
s->sock.f = (Handle)srvaccept;
s->conns.less = conn_less;
s->conns.setpos = conn_setpos;
if (sockwant(&s->sock, 'r') == -1) {
twarn("sockwant");
exit(2);
}
for (;;) {
int64 period = prottick(s);
int rw = socknext(&sock, period);
if (rw == -1) {
twarnx("socknext");
exit(1);
}
if (rw) {
sock->f(sock->x, rw);
}
}
}
void
srvaccept(Server *s, int ev)
{
h_accept(s->sock.fd, ev, s);
}
+91
View File
@@ -0,0 +1,91 @@
#include <stdint.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <port.h>
#include "dat.h"
static int portfd;
int
sockinit(void)
{
portfd = port_create();
if (portfd == -1) {
twarn("port_create");
return -1;
}
return 0;
}
int
sockwant(Socket *s, int rw)
{
int events = 0;
if (rw) {
switch (rw) {
case 'r':
events |= POLLIN;
break;
case 'w':
events |= POLLOUT;
break;
}
}
events |= POLLPRI;
if (!s->added && !rw) {
return 0;
} else if (!s->added && rw) {
s->added = 1;
return port_associate(portfd, PORT_SOURCE_FD, s->fd, events, (void *)s);
} else if (!rw) {
return port_dissociate(portfd, PORT_SOURCE_FD, s->fd);
} else {
port_dissociate(portfd, PORT_SOURCE_FD, s->fd);
return port_associate(portfd, PORT_SOURCE_FD, s->fd, events, (void *)s);
}
}
int
socknext(Socket **s, int64 timeout)
{
int r;
uint_t n = 1;
struct port_event pe;
struct timespec ts;
ts.tv_sec = timeout / 1000000000;
ts.tv_nsec = timeout % 1000000000;
r = port_getn(portfd, &pe, 1, &n, &ts);
if (r == -1 && errno != ETIME && errno != EINTR) {
twarn("port_getn");
return -1;
}
if (r == 0) {
*s = pe.portev_user;
if (pe.portev_events & POLLHUP) {
return 'h';
} else if (pe.portev_events & POLLIN) {
if (sockwant(*s, 'r') == -1) {
return -1;
}
return 'r';
} else if (pe.portev_events & POLLOUT) {
if (sockwant(*s, 'w') == -1) {
return -1;
}
return 'w';
}
}
return 0;
}
+271
View File
@@ -0,0 +1,271 @@
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include "ct/ct.h"
void
cttest_heap_insert_one()
{
Heap h = {
.less = job_pri_less,
.setpos = job_setpos,
};
Job *j = make_job(1, 0, 1, 0, 0);
assertf(j, "allocate job");
heapinsert(&h, j);
assertf(h.len == 1, "h should contain one item.");
assertf(j->heap_index == 0, "should match");
assert(heapremove(&h, 0));
job_free(j);
free(h.data);
}
void
cttest_heap_insert_and_remove_one()
{
Heap h = {
.less = job_pri_less,
.setpos = job_setpos,
};
Job *j1 = make_job(1, 0, 1, 0, 0);
assertf(j1, "allocate job");
int r = heapinsert(&h, j1);
assertf(r, "insert should succeed");
Job *got = heapremove(&h, 0);
assertf(got == j1, "j1 should come back out");
assertf(h.len == 0, "h should be empty.");
free(h.data);
job_free(j1);
}
void
cttest_heap_priority()
{
Heap h = {
.less = job_pri_less,
.setpos = job_setpos,
};
Job *j, *j1, *j2, *j3;
j1 = make_job(1, 0, 1, 0, 0);
j2 = make_job(2, 0, 1, 0, 0);
j3 = make_job(3, 0, 1, 0, 0);
assertf(j1, "allocate job");
assertf(j2, "allocate job");
assertf(j3, "allocate job");
int r = heapinsert(&h, j2);
assertf(r, "insert should succeed");
assertf(j2->heap_index == 0, "should match");
r = heapinsert(&h, j3);
assertf(r, "insert should succeed");
assertf(j2->heap_index == 0, "should match");
assertf(j3->heap_index == 1, "should match");
r = heapinsert(&h, j1);
assertf(r, "insert should succeed");
assertf(j1->heap_index == 0, "should match");
assertf(j2->heap_index == 2, "should match");
assertf(j3->heap_index == 1, "should match");
j = heapremove(&h, 0);
assertf(j == j1, "j1 should come out first.");
assertf(j2->heap_index == 0, "should match");
assertf(j3->heap_index == 1, "should match");
j = heapremove(&h, 0);
assertf(j == j2, "j2 should come out second.");
assertf(j3->heap_index == 0, "should match");
j = heapremove(&h, 0);
assertf(j == j3, "j3 should come out third.");
free(h.data);
job_free(j1);
job_free(j2);
job_free(j3);
}
void
cttest_heap_fifo_property()
{
Heap h = {
.less = job_pri_less,
.setpos = job_setpos,
};
Job *j, *j3a, *j3b, *j3c;
j3a = make_job(3, 0, 1, 0, 0);
j3b = make_job(3, 0, 1, 0, 0);
j3c = make_job(3, 0, 1, 0, 0);
assertf(j3a, "allocate job");
assertf(j3b, "allocate job");
assertf(j3c, "allocate job");
int r = heapinsert(&h, j3a);
assertf(r, "insert should succeed");
assertf(h.data[0] == j3a, "j3a should be in pos 0");
assertf(j3a->heap_index == 0, "should match");
r = heapinsert(&h, j3b);
assertf(r, "insert should succeed");
assertf(h.data[1] == j3b, "j3b should be in pos 1");
assertf(j3a->heap_index == 0, "should match");
assertf(j3b->heap_index == 1, "should match");
r = heapinsert(&h, j3c);
assertf(r, "insert should succeed");
assertf(h.data[2] == j3c, "j3c should be in pos 2");
assertf(j3a->heap_index == 0, "should match");
assertf(j3b->heap_index == 1, "should match");
assertf(j3c->heap_index == 2, "should match");
j = heapremove(&h, 0);
assertf(j == j3a, "j3a should come out first.");
assertf(j3b->heap_index == 0, "should match");
assertf(j3c->heap_index == 1, "should match");
j = heapremove(&h, 0);
assertf(j == j3b, "j3b should come out second.");
assertf(j3c->heap_index == 0, "should match");
j = heapremove(&h, 0);
assertf(j == j3c, "j3c should come out third.");
free(h.data);
job_free(j3a);
job_free(j3b);
job_free(j3c);
}
void
cttest_heap_many_jobs()
{
Heap h = {
.less = job_pri_less,
.setpos = job_setpos,
};
const int n = 20;
Job *j;
int i;
for (i = 0; i < n; i++) {
j = make_job(1 + rand() % 8192, 0, 1, 0, 0);
assertf(j, "allocation");
int r = heapinsert(&h, j);
assertf(r, "heapinsert");
}
uint last_pri = 0;
for (i = 0; i < n; i++) {
j = heapremove(&h, 0);
assertf(j->r.pri >= last_pri, "should come out in order");
last_pri = j->r.pri;
assert(j);
job_free(j);
}
free(h.data);
}
void
cttest_heap_remove_k()
{
Heap h = {
.less = job_pri_less,
.setpos = job_setpos,
};
const int n = 50;
const int mid = 25;
int c, i;
for (c = 0; c < 50; c++) {
for (i = 0; i < n; i++) {
Job *j = make_job(1 + rand() % 8192, 0, 1, 0, 0);
assertf(j, "allocation");
int r = heapinsert(&h, j);
assertf(r, "heapinsert");
}
/* remove one from the middle */
Job *j0 = heapremove(&h, mid);
assertf(j0, "j0 should not be NULL");
job_free(j0);
/* now make sure the rest are still a valid heap */
uint last_pri = 0;
for (i = 1; i < n; i++) {
Job *j = heapremove(&h, 0);
assertf(j->r.pri >= last_pri, "should come out in order");
last_pri = j->r.pri;
assertf(j, "j should not be NULL");
job_free(j);
}
}
free(h.data);
}
void
ctbench_heap_insert(int n)
{
Job **j = calloc(n, sizeof *j);
int i;
for (i = 0; i < n; i++) {
j[i] = make_job(1, 0, 1, 0, 0);
assert(j[i]);
j[i]->r.pri = -j[i]->r.id;
}
Heap h = {
.less = job_pri_less,
.setpos = job_setpos,
};
ctresettimer();
for (i = 0; i < n; i++) {
heapinsert(&h, j[i]);
}
ctstoptimer();
for (i = 0; i < n; i++)
job_free(heapremove(&h, 0));
free(h.data);
free(j);
}
void
ctbench_heap_remove(int n)
{
Heap h = {
.less = job_pri_less,
.setpos = job_setpos,
};
int i;
for (i = 0; i < n; i++) {
Job *j = make_job(1, 0, 1, 0, 0);
assertf(j, "allocate job");
heapinsert(&h, j);
}
Job **jj = calloc(n, sizeof(Job *)); // temp storage to deallocate jobs later
ctresettimer();
for (i = 0; i < n; i++) {
jj[i] = (Job *)heapremove(&h, 0);
}
ctstoptimer();
free(h.data);
for (i = 0; i < n; i++)
job_free(jj[i]);
free(jj);
}
+166
View File
@@ -0,0 +1,166 @@
#include "ct/ct.h"
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
static Tube *default_tube;
void
cttest_job_creation()
{
Job *j;
TUBE_ASSIGN(default_tube, make_tube("default"));
j = make_job(1, 0, 1, 0, default_tube);
assertf(j->r.pri == 1, "priority should match");
}
void
cttest_job_cmp_pris()
{
Job *a, *b;
TUBE_ASSIGN(default_tube, make_tube("default"));
a = make_job(1, 0, 1, 0, default_tube);
b = make_job(1 << 27, 0, 1, 0, default_tube);
assertf(job_pri_less(a, b), "should be less");
}
void
cttest_job_cmp_ids()
{
Job *a, *b;
TUBE_ASSIGN(default_tube, make_tube("default"));
a = make_job(1, 0, 1, 0, default_tube);
b = make_job(1, 0, 1, 0, default_tube);
b->r.id <<= 49;
assertf(job_pri_less(a, b), "should be less");
}
void
cttest_job_large_pris()
{
Job *a, *b;
TUBE_ASSIGN(default_tube, make_tube("default"));
a = make_job(1, 0, 1, 0, default_tube);
b = make_job(-5, 0, 1, 0, default_tube);
assertf(job_pri_less(a, b), "should be less");
a = make_job(-5, 0, 1, 0, default_tube);
b = make_job(1, 0, 1, 0, default_tube);
assertf(!job_pri_less(a, b), "should not be less");
}
void
cttest_job_hash_free()
{
Job *j;
uint64 jid = 83;
TUBE_ASSIGN(default_tube, make_tube("default"));
j = make_job_with_id(0, 0, 1, 0, default_tube, jid);
job_free(j);
assertf(!job_find(jid), "job should be missing");
}
void
cttest_job_hash_free_next()
{
Job *a, *b;
uint64 aid = 97, bid = 12386;
TUBE_ASSIGN(default_tube, make_tube("default"));
b = make_job_with_id(0, 0, 1, 0, default_tube, bid);
a = make_job_with_id(0, 0, 1, 0, default_tube, aid);
assertf(a->ht_next == b, "b should be chained to a");
job_free(b);
assertf(a->ht_next == NULL, "job should be missing");
}
void
cttest_job_all_jobs_used()
{
Job *j, *x;
TUBE_ASSIGN(default_tube, make_tube("default"));
j = make_job(0, 0, 1, 0, default_tube);
assertf(get_all_jobs_used() == 1, "should match");
x = allocate_job(10);
assertf(get_all_jobs_used() == 1, "should match");
job_free(x);
assertf(get_all_jobs_used() == 1, "should match");
job_free(j);
assertf(get_all_jobs_used() == 0, "should match");
}
void
cttest_job_100_000_jobs()
{
int i;
TUBE_ASSIGN(default_tube, make_tube("default"));
for (i = 0; i < 100000; i++) {
make_job(0, 0, 1, 0, default_tube);
}
assertf(get_all_jobs_used() == 100000, "should match");
for (i = 1; i <= 100000; i++) {
job_free(job_find(i));
}
fprintf(stderr, "get_all_jobs_used() => %zu\n", get_all_jobs_used());
assertf(get_all_jobs_used() == 0, "should match");
}
void
ctbench_job_make(int n)
{
int i;
Job **j = calloc(n, sizeof *j);
TUBE_ASSIGN(default_tube, make_tube("default"));
ctresettimer();
for (i = 0; i < n; i++) {
j[i] = make_job(0, 0, 1, 0, default_tube);
}
ctstoptimer();
for (i = 0; i < n; i++) {
job_free(j[i]);
}
free(j);
}
void
ctbench_job_free(int n)
{
int i;
Job **j = calloc(n, sizeof *j);
TUBE_ASSIGN(default_tube, make_tube("default"));
for (i = 0; i < n; i++) {
j[i] = make_job(0, 0, 1, 0, default_tube);
}
ctresettimer();
for (i = 0; i < n; i++) {
job_free(j[i]);
}
ctstoptimer();
free(j);
}
+139
View File
@@ -0,0 +1,139 @@
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include "ct/ct.h"
void
cttest_ms_append()
{
Ms *a = new(struct Ms);
ms_init(a, NULL, NULL);
int i = 10;
int ok = ms_append(a, &i);
assertf(a->len == 1, "a should contain one item");
assertf(ok, "should be added");
ok = ms_append(a, &i);
assertf(a->len == 2, "a should contain two items");
assertf(ok, "should be added");
ms_clear(a);
assertf(a->len == 0, "a should be empty");
free(a->items);
free(a);
}
void
cttest_ms_remove()
{
Ms *a = new(struct Ms);
ms_init(a, NULL, NULL);
int i = 1;
ms_append(a, &i);
int j = 2;
int ok = ms_remove(a, &j);
assertf(!ok, "j should not be removed");
ok = ms_remove(a, &i);
assertf(ok, "i should be removed");
ok = ms_remove(a, &i);
assertf(!ok, "i was already removed");
assertf(a->len == 0, "a should be empty");
free(a->items);
free(a);
}
void
cttest_ms_contains()
{
Ms *a = new(struct Ms);
ms_init(a, NULL, NULL);
int i = 1;
ms_append(a, &i);
int ok = ms_contains(a, &i);
assertf(ok, "i should be in a");
int j = 2;
ok = ms_contains(a, &j);
assertf(!ok, "j should not be in a");
ms_clear(a);
free(a->items);
free(a);
}
void
cttest_ms_clear_empty()
{
Ms *a = new(struct Ms);
ms_init(a, NULL, NULL);
ms_clear(a);
assertf(a->len == 0, "a should be empty");
free(a->items);
free(a);
}
void
cttest_ms_take()
{
Ms *a = new(struct Ms);
ms_init(a, NULL, NULL);
int i = 10;
int j = 20;
ms_append(a, &i);
ms_append(a, &j);
int *n;
n = (int *)ms_take(a);
assertf(n == &i, "n should point to i");
n = (int *)ms_take(a);
assertf(n == &j, "n should point to j");
n = (int *)ms_take(a);
assertf(n == NULL, "n should be NULL; ms is empty");
free(a->items);
free(a);
}
void
cttest_ms_take_sequence()
{
size_t i;
int s[] = {1, 2, 3, 4, 5, 6};
int e[] = {1, 2, 3, 6, 5, 4};
Ms *a = new(struct Ms);
ms_init(a, NULL, NULL);
size_t n = sizeof(s)/sizeof(s[0]);
for (i = 0; i < n; i++)
ms_append(a, &s[i]);
for (i = 0; i < n; i++) {
int *got = (int *)ms_take(a);
assert(*got == e[i]);
}
free(a->items);
free(a);
}
+2094
View File
File diff suppressed because it is too large Load Diff
+232
View File
@@ -0,0 +1,232 @@
#include "ct/ct.h"
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>
void
cttest_allocf()
{
char *got;
got = fmtalloc("hello, %s %d", "world", 5);
assertf(strcmp("hello, world 5", got) == 0, "got \"%s\"", got);
free(got);
}
void
cttest_opt_none()
{
char *args[] = {
NULL,
};
optparse(&srv, args);
assert(strcmp(srv.port, Portdef) == 0);
assert(srv.addr == NULL);
assert(job_data_size_limit == JOB_DATA_SIZE_LIMIT_DEFAULT);
assert(srv.wal.filesize == Filesizedef);
assert(srv.wal.wantsync == 1);
assert(srv.wal.syncrate == DEFAULT_FSYNC_MS*1000000);
assert(srv.user == NULL);
assert(srv.wal.dir == NULL);
assert(srv.wal.use == 0);
assert(verbose == 0);
}
static void
success(void)
{
_exit(0);
}
void
cttest_optminus()
{
char *args[] = {
"-",
NULL,
};
atexit(success);
optparse(&srv, args);
assertf(0, "optparse failed to call exit");
}
void
cttest_optp()
{
char *args[] = {
"-p1234",
NULL,
};
optparse(&srv, args);
assert(strcmp(srv.port, "1234") == 0);
}
void
cttest_optl()
{
char *args[] = {
"-llocalhost",
NULL,
};
optparse(&srv, args);
assert(strcmp(srv.addr, "localhost") == 0);
}
void
cttest_optlseparate()
{
char *args[] = {
"-l",
"localhost",
NULL,
};
optparse(&srv, args);
assert(strcmp(srv.addr, "localhost") == 0);
}
void
cttest_optz()
{
char *args[] = {
"-z1234",
NULL,
};
optparse(&srv, args);
assert(job_data_size_limit == 1234);
}
void
cttest_optz_more_than_max()
{
char *args[] = {
"-z1073741825",
NULL,
};
optparse(&srv, args);
assert(job_data_size_limit == 1073741824);
}
void
cttest_opts()
{
char *args[] = {
"-s1234",
NULL,
};
optparse(&srv, args);
assert(srv.wal.filesize == 1234);
}
void
cttest_optf()
{
char *args[] = {
"-f1234",
NULL,
};
optparse(&srv, args);
assert(srv.wal.syncrate == 1234000000);
assert(srv.wal.wantsync == 1);
}
void
cttest_optF()
{
char *args[] = {
"-f1234",
"-F",
NULL,
};
optparse(&srv, args);
assert(srv.wal.wantsync == 0);
}
void
cttest_optu()
{
char *args[] = {
"-ukr",
NULL,
};
optparse(&srv, args);
assert(strcmp(srv.user, "kr") == 0);
}
void
cttest_optb()
{
char *args[] = {
"-bfoo",
NULL,
};
optparse(&srv, args);
assert(strcmp(srv.wal.dir, "foo") == 0);
assert(srv.wal.use == 1);
}
void
cttest_optV()
{
char *args[] = {
"-V",
NULL,
};
optparse(&srv, args);
assert(verbose == 1);
}
void
cttest_optV_V()
{
char *args[] = {
"-V",
"-V",
NULL,
};
optparse(&srv, args);
assert(verbose == 2);
}
void
cttest_optVVV()
{
char *args[] = {
"-VVV",
NULL,
};
optparse(&srv, args);
assert(verbose == 3);
}
void
cttest_optVFVu()
{
char *args[] = {
"-VFVukr",
NULL,
};
optparse(&srv, args);
assert(verbose == 2);
assert(srv.wal.wantsync == 0);
assert(strcmp(srv.user, "kr") == 0);
}
+16
View File
@@ -0,0 +1,16 @@
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <sys/time.h>
int64
nanoseconds(void)
{
int r;
struct timeval tv;
r = gettimeofday(&tv, 0);
if (r != 0) return warnx("gettimeofday"), -1; // can't happen
return ((int64)tv.tv_sec)*1000000000 + ((int64)tv.tv_usec)*1000;
}
+105
View File
@@ -0,0 +1,105 @@
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
struct Ms tubes;
Tube *
make_tube(const char *name)
{
Tube *t = new(Tube);
if (!t)
return NULL;
strncpy(t->name, name, MAX_TUBE_NAME_LEN);
if (t->name[MAX_TUBE_NAME_LEN - 1] != '\0') {
t->name[MAX_TUBE_NAME_LEN - 1] = '\0';
twarnx("truncating tube name");
}
t->ready.less = job_pri_less;
t->delay.less = job_delay_less;
t->ready.setpos = job_setpos;
t->delay.setpos = job_setpos;
Job j = {.tube = NULL};
t->buried = j;
t->buried.prev = t->buried.next = &t->buried;
ms_init(&t->waiting_conns, NULL, NULL);
return t;
}
static void
tube_free(Tube *t)
{
ms_remove(&tubes, t);
free(t->ready.data);
free(t->delay.data);
ms_clear(&t->waiting_conns);
free(t);
}
void
tube_dref(Tube *t)
{
if (!t) return;
if (t->refs < 1) {
twarnx("refs is zero for tube: %s", t->name);
return;
}
--t->refs;
if (t->refs < 1)
tube_free(t);
}
void
tube_iref(Tube *t)
{
if (!t) return;
++t->refs;
}
static Tube *
make_and_insert_tube(const char *name)
{
int r;
Tube *t = NULL;
t = make_tube(name);
if (!t)
return NULL;
/* We want this global tube list to behave like "weak" refs, so don't
* increment the ref count. */
r = ms_append(&tubes, t);
if (!r)
return tube_dref(t), (Tube *) 0;
return t;
}
Tube *
tube_find(Ms *tubeset, const char *name)
{
size_t i;
for (i = 0; i < tubeset->len; i++) {
Tube *t = tubeset->items[i];
if (strncmp(t->name, name, MAX_TUBE_NAME_LEN) == 0)
return t;
}
return NULL;
}
Tube *
tube_find_or_make(const char *name)
{
Tube *t = tube_find(&tubes, name);
if (t)
return t;
return make_and_insert_tube(name);
}
+222
View File
@@ -0,0 +1,222 @@
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#ifdef HAVE_LIBSYSTEMD
#include <systemd/sd-daemon.h>
#endif
const char *progname;
static void
vwarnx(const char *err, const char *fmt, va_list args)
__attribute__((format(printf, 2, 0)));
static void
vwarnx(const char *err, const char *fmt, va_list args)
{
fprintf(stderr, "%s: ", progname);
if (fmt) {
vfprintf(stderr, fmt, args);
if (err) fprintf(stderr, ": %s", err);
}
fputc('\n', stderr);
}
void
warn(const char *fmt, ...)
{
char *err = strerror(errno); /* must be done first thing */
va_list args;
va_start(args, fmt);
vwarnx(err, fmt, args);
va_end(args);
}
void
warnx(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
vwarnx(NULL, fmt, args);
va_end(args);
}
char*
fmtalloc(char *fmt, ...)
{
int n;
char *buf;
va_list ap;
// find out how much space is needed
va_start(ap, fmt);
n = vsnprintf(0, 0, fmt, ap) + 1; // include space for trailing NUL
va_end(ap);
buf = malloc(n);
if (buf) {
va_start(ap, fmt);
vsnprintf(buf, n, fmt, ap);
va_end(ap);
}
return buf;
}
// Zalloc allocates n bytes of zeroed memory and
// returns a pointer to it.
// If insufficient memory is available, zalloc returns 0.
void*
zalloc(int n)
{
void *p;
p = malloc(n);
if (p) {
memset(p, 0, n);
}
return p;
}
static void
warn_systemd_ignored_option(char *opt, char *arg)
{
#ifdef HAVE_LIBSYSTEMD
if (sd_listen_fds(0) > 0) {
warnx("inherited listen fd; ignoring option: %s %s", opt, arg);
}
#endif
}
static void usage(int code) __attribute__ ((noreturn));
static void
usage(int code)
{
fprintf(stderr, "Use: %s [OPTIONS]\n"
"\n"
"Options:\n"
" -b DIR write-ahead log directory\n"
" -f MS fsync at most once every MS milliseconds (default is %dms);\n"
" use -f0 for \"always fsync\"\n"
" -F never fsync\n"
" -l ADDR listen on address (default is 0.0.0.0)\n"
" -p PORT listen on port (default is " Portdef ")\n"
" -u USER become user and group\n"
" -z BYTES set the maximum job size in bytes (default is %d);\n"
" max allowed is %d bytes\n"
" -s BYTES set the size of each write-ahead log file (default is %d);\n"
" will be rounded up to a multiple of 4096 bytes\n"
" -v show version information\n"
" -V increase verbosity\n"
" -h show this help\n",
progname,
DEFAULT_FSYNC_MS,
JOB_DATA_SIZE_LIMIT_DEFAULT,
JOB_DATA_SIZE_LIMIT_MAX,
Filesizedef);
exit(code);
}
static char *flagusage(char *flag) __attribute__ ((noreturn));
static char *
flagusage(char *flag)
{
warnx("flag requires an argument: %s", flag);
usage(5);
}
static size_t
parse_size_t(char *str)
{
char r, x;
size_t size;
r = sscanf(str, "%zu%c", &size, &x);
if (1 != r) {
warnx("invalid size: %s", str);
usage(5);
}
return size;
}
void
optparse(Server *s, char **argv)
{
int64 ms;
char *arg, *tmp;
# define EARGF(x) (*arg ? (tmp=arg,arg="",tmp) : *argv ? *argv++ : (x))
while ((arg = *argv++) && *arg++ == '-' && *arg) {
char c;
while ((c = *arg++)) {
switch (c) {
case 'p':
s->port = EARGF(flagusage("-p"));
warn_systemd_ignored_option("-p", s->port);
break;
case 'l':
s->addr = EARGF(flagusage("-l"));
warn_systemd_ignored_option("-l", s->addr);
break;
case 'z':
job_data_size_limit = parse_size_t(EARGF(flagusage("-z")));
if (job_data_size_limit > JOB_DATA_SIZE_LIMIT_MAX) {
warnx("maximum job size was set to %d", JOB_DATA_SIZE_LIMIT_MAX);
job_data_size_limit = JOB_DATA_SIZE_LIMIT_MAX;
}
break;
case 's':
s->wal.filesize = parse_size_t(EARGF(flagusage("-s")));
break;
case 'c':
warnx("-c flag was removed. binlog is always compacted.");
break;
case 'n':
warnx("-n flag was removed. binlog is always compacted.");
break;
case 'f':
ms = (int64)parse_size_t(EARGF(flagusage("-f")));
s->wal.syncrate = ms * 1000000;
s->wal.wantsync = 1;
break;
case 'F':
s->wal.wantsync = 0;
break;
case 'u':
s->user = EARGF(flagusage("-u"));
break;
case 'b':
s->wal.dir = EARGF(flagusage("-b"));
s->wal.use = 1;
break;
case 'h':
usage(0);
case 'v':
printf("beanstalkd %s\n", version);
exit(0);
case 'V':
verbose++;
break;
default:
warnx("unknown flag: %s", arg-2);
usage(5);
}
}
}
if (arg) {
warnx("unknown argument: %s", arg-1);
usage(5);
}
}
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
printf 'const char version[] = "'
./vers.sh
printf '";\n'
+1
View File
@@ -0,0 +1 @@
printf '1.13'
+492
View File
@@ -0,0 +1,492 @@
#include "dat.h"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
#include <sys/uio.h>
#include <sys/stat.h>
#include <limits.h>
static int reserve(Wal *w, int n);
// Reads w->dir for files matching binlog.NNN,
// sets w->next to the next unused number, and
// returns the minimum number.
// If no files are found, sets w->next to 1 and
// returns a large number.
static int
walscandir(Wal *w)
{
static char base[] = "binlog.";
static const int len = sizeof(base) - 1;
DIR *d;
struct dirent *e;
int min = 1<<30;
int max = 0;
int n;
char *p;
d = opendir(w->dir);
if (!d) return min;
while ((e = readdir(d))) {
if (strncmp(e->d_name, base, len) == 0) {
n = strtol(e->d_name+len, &p, 10);
if (p && *p == '\0') {
if (n > max) max = n;
if (n < min) min = n;
}
}
}
closedir(d);
w->next = max + 1;
return min;
}
void
walgc(Wal *w)
{
File *f;
while (w->head && !w->head->refs) {
f = w->head;
w->head = f->next;
if (w->tail == f) {
w->tail = f->next; // also, f->next == NULL
}
w->nfile--;
unlink(f->path);
free(f->path);
free(f);
}
}
// returns 1 on success, 0 on error.
static int
usenext(Wal *w)
{
File *f;
f = w->cur;
if (!f->next) {
twarnx("there is no next wal file");
return 0;
}
w->cur = f->next;
filewclose(f);
return 1;
}
static int
ratio(Wal *w)
{
int64 n, d;
d = w->alive + w->resv;
n = (int64)w->nfile * (int64)w->filesize - d;
if (!d) return 0;
return n / d;
}
// Returns the number of bytes reserved or 0 on error.
static int
walresvmigrate(Wal *w, Job *j)
{
int z = 0;
// reserve only space for the migrated full job record
// space for the delete is already reserved
z += sizeof(int);
z += strlen(j->tube->name);
z += sizeof(Jobrec);
z += j->r.body_size;
return reserve(w, z);
}
static void
moveone(Wal *w)
{
Job *j;
if (w->head == w->cur || w->head->next == w->cur) {
// no point in moving a job
return;
}
j = w->head->jlist.fnext;
if (!j || j == &w->head->jlist) {
// head holds no jlist; can't happen
twarnx("head holds no jlist");
return;
}
if (!walresvmigrate(w, j)) {
// it will not fit, so we'll try again later
return;
}
filermjob(w->head, j);
w->nmig++;
walwrite(w, j);
}
static void
walcompact(Wal *w)
{
int r;
for (r=ratio(w); r>=2; r--) {
moveone(w);
}
}
static void
walsync(Wal *w)
{
int64 now;
now = nanoseconds();
if (w->wantsync && now >= w->lastsync+w->syncrate) {
w->lastsync = now;
if (fsync(w->cur->fd) == -1) {
twarn("fsync");
}
}
}
// Walwrite writes j to the log w (if w is enabled).
// On failure, walwrite disables w and returns 0; on success, it returns 1.
// Unlke walresv*, walwrite should never fail because of a full disk.
// If w is disabled, then walwrite takes no action and returns 1.
int
walwrite(Wal *w, Job *j)
{
int r = 0;
if (!w->use) return 1;
if (w->cur->resv > 0 || usenext(w)) {
if (j->file) {
r = filewrjobshort(w->cur, j);
} else {
r = filewrjobfull(w->cur, j);
}
}
if (!r) {
filewclose(w->cur);
w->use = 0;
}
w->nrec++;
return r;
}
void
walmaint(Wal *w)
{
if (w->use) {
walcompact(w);
walsync(w);
}
}
static int
makenextfile(Wal *w)
{
File *f;
f = new(File);
if (!f) {
twarnx("OOM");
return 0;
}
if (!fileinit(f, w, w->next)) {
free(f);
twarnx("OOM");
return 0;
}
filewopen(f);
if (!f->iswopen) {
free(f->path);
free(f);
return 0;
}
w->next++;
fileadd(f, w);
return 1;
}
static void
moveresv(File *to, File *from, int n)
{
from->resv -= n;
from->free += n;
to->resv += n;
to->free -= n;
}
static int
needfree(Wal *w, int n)
{
if (w->tail->free >= n) return n;
if (makenextfile(w)) return n;
return 0;
}
// Ensures:
// 1. b->resv is congruent to n (mod z).
// 2. x->resv is congruent to 0 (mod z) for each future file x.
// Assumes (and preserves) that b->resv >= n.
// Reserved space is conserved (neither created nor destroyed);
// we just move it around to preserve the invariant.
// We might have to allocate a new file.
// Returns 1 on success, otherwise 0. If there was a failure,
// w->tail is not updated.
static int
balancerest(Wal *w, File *b, int n)
{
int rest, c, r;
static const int z = sizeof(int) + sizeof(Jobrec);
if (!b) return 1;
rest = b->resv - n;
r = rest % z;
if (r == 0) return balancerest(w, b->next, 0);
c = z - r;
if (w->tail->resv >= c && b->free >= c) {
moveresv(b, w->tail, c);
return balancerest(w, b->next, 0);
}
if (needfree(w, r) != r) {
twarnx("needfree");
return 0;
}
moveresv(w->tail, b, r);
return balancerest(w, b->next, 0);
}
// Ensures:
// 1. w->cur->resv >= n.
// 2. w->cur->resv is congruent to n (mod z).
// 3. x->resv is congruent to 0 (mod z) for each future file x.
// (where z is the size of a delete record in the wal).
// Reserved space is conserved (neither created nor destroyed);
// we just move it around to preserve the invariant.
// We might have to allocate a new file.
// Returns 1 on success, otherwise 0. If there was a failure,
// w->tail is not updated.
static int
balance(Wal *w, int n)
{
// Invariant 1
// (this loop will run at most once)
while (w->cur->resv < n) {
int m = w->cur->resv;
int r = needfree(w, m);
if (r != m) {
twarnx("needfree");
return 0;
}
moveresv(w->tail, w->cur, m);
usenext(w);
}
// Invariants 2 and 3
return balancerest(w, w->cur, n);
}
// Returns the number of bytes successfully reserved: either 0 or n.
static int
reserve(Wal *w, int n)
{
int r;
// return value must be nonzero but is otherwise ignored
if (!w->use) return 1;
if (w->cur->free >= n) {
w->cur->free -= n;
w->cur->resv += n;
w->resv += n;
return n;
}
r = needfree(w, n);
if (r != n) {
twarnx("needfree");
return 0;
}
w->tail->free -= n;
w->tail->resv += n;
w->resv += n;
if (!balance(w, n)) {
// error; undo the reservation
w->resv -= n;
w->tail->resv -= n;
w->tail->free += n;
return 0;
}
return n;
}
// Returns the number of bytes reserved or 0 on error.
int
walresvput(Wal *w, Job *j)
{
int z = 0;
// reserve space for the initial job record
z += sizeof(int);
z += strlen(j->tube->name);
z += sizeof(Jobrec);
z += j->r.body_size;
// plus space for a delete to come later
z += sizeof(int);
z += sizeof(Jobrec);
return reserve(w, z);
}
// Returns the number of bytes reserved or 0 on error.
int
walresvupdate(Wal *w)
{
int z = 0;
z +=sizeof(int);
z +=sizeof(Jobrec);
return reserve(w, z);
}
// Returns the number of locks acquired: either 0 or 1.
int
waldirlock(Wal *w)
{
int r;
int fd;
struct flock lk;
char *path;
size_t path_length;
path_length = strlen(w->dir) + strlen("/lock") + 1;
if ((path = malloc(path_length)) == NULL) {
twarn("malloc");
return 0;
}
snprintf(path, path_length, "%s/lock", w->dir);
fd = open(path, O_WRONLY|O_CREAT, 0600);
free(path);
if (fd == -1) {
twarn("open");
return 0;
}
lk.l_type = F_WRLCK;
lk.l_whence = SEEK_SET;
lk.l_start = 0;
lk.l_len = 0;
r = fcntl(fd, F_SETLK, &lk);
if (r) {
twarn("fcntl");
return 0;
}
// intentionally leak fd, since we never want to close it
// and we'll never need it again
return 1;
}
void
walread(Wal *w, Job *list, int min)
{
int i;
int err = 0;
for (i = min; i < w->next; i++) {
File *f = new(File);
if (!f) {
twarnx("OOM");
exit(1);
}
if (!fileinit(f, w, i)) {
free(f);
twarnx("OOM");
exit(1);
}
int fd = open(f->path, O_RDONLY);
if (fd < 0) {
twarn("open %s", f->path);
free(f->path);
free(f);
continue;
}
f->fd = fd;
fileadd(f, w);
err |= fileread(f, list);
if (close(fd) == -1)
twarn("close");
}
if (err) {
warnx("Errors reading one or more WAL files.");
warnx("Continuing. You may be missing data.");
}
}
void
walinit(Wal *w, Job *list)
{
int min;
min = walscandir(w);
walread(w, list, min);
// first writable file
if (!makenextfile(w)) {
twarnx("makenextfile");
exit(1);
}
w->cur = w->tail;
}