{"id":36444913,"url":"https://github.com/simplegear/rate-envelope-queue","last_synced_at":"2026-01-11T22:04:05.936Z","repository":{"id":311274396,"uuid":"1043139132","full_name":"simplegear/rate-envelope-queue","owner":"simplegear","description":"Lightweight in-memory queue + scheduler for Go with middleware-style hooks and capacity/backpressure control.","archived":false,"fork":false,"pushed_at":"2025-10-25T10:32:40.000Z","size":135,"stargazers_count":20,"open_issues_count":10,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-25T12:22:45.718Z","etag":null,"topics":["background-jobs","background-processing","background-tasks","backpressure","concurrency","cronjob","go","golang","hooks","in-memory","in-memory-queue","job-scheduler","middleware","queue","rate-envelope-queue","rate-limiting","scheduler","task-scheduler","worker-pool","workqueue"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/simplegear.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-23T08:12:22.000Z","updated_at":"2025-09-29T06:24:29.000Z","dependencies_parsed_at":"2025-08-23T19:06:04.807Z","dependency_job_id":"9035acf3-bedb-44db-9ded-605c486b0790","html_url":"https://github.com/simplegear/rate-envelope-queue","commit_stats":null,"previous_names":["pavelagarkov/rate-pool","simplegear/rate-envelope-queue"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/simplegear/rate-envelope-queue","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplegear%2Frate-envelope-queue","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplegear%2Frate-envelope-queue/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplegear%2Frate-envelope-queue/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplegear%2Frate-envelope-queue/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/simplegear","download_url":"https://codeload.github.com/simplegear/rate-envelope-queue/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplegear%2Frate-envelope-queue/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28324855,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-11T18:42:50.174Z","status":"ssl_error","status_checked_at":"2026-01-11T18:39:13.842Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["background-jobs","background-processing","background-tasks","backpressure","concurrency","cronjob","go","golang","hooks","in-memory","in-memory-queue","job-scheduler","middleware","queue","rate-envelope-queue","rate-limiting","scheduler","task-scheduler","worker-pool","workqueue"],"created_at":"2026-01-11T22:04:05.871Z","updated_at":"2026-01-11T22:04:05.930Z","avatar_url":"https://github.com/simplegear.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rate-envelope-queue\n\n\u003e A lightweight, goroutine-safe wrapper around `k8s.io/client-go/util/workqueue` for managing tasks (**envelopes**) with a fixed worker pool, periodic scheduling, deadlines, hooks, and **stamps (middleware)**. Adds a safe queue lifecycle (**Start/Stop/Start**), buffering before first start, and queue capacity limiting.\n\n\u003e Under the hood it uses `workqueue.TypedRateLimitingInterface`. Deduplication happens by **pointer** to the element: repeated `Add` of the *same pointer* while it is in-flight is ignored.\n\n---\n\n## Table of Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Concepts \u0026 Contracts](#concepts--contracts)\n    - [Envelope](#envelope)\n    - [Queue](#queue)\n    - [Stamps (middleware)](#stamps-middleware)\n- [Worker Behavior](#worker-behavior)\n- [Stop Modes](#stop-modes)\n- [Capacity Limiting](#capacity-limiting)\n- [Benchmarks](#benchmarks)\n- [Metrics (Prometheus)](#metrics-prometheus)\n- [Examples](#examples)\n- [License](#license)\n\n---\n\n## Features\n\nGoroutine-safe **in-memory** queue for your service:\n\n- All public methods are safe to call from multiple goroutines. `Send` can be called concurrently. Multiple workers are supported via `limit`. `Start/Stop` are serialized internally.\n- This is **not** a distributed queue: there are no guarantees across processes/hosts. Ensure your hooks/invokers are thread-safe around shared state.\n\nWhat you get:\n\n- A clear lifecycle FSM: `init → running → stopping → stopped`\n- **Both one-off and periodic** tasks\n- **Middleware chain** via `Stamp`\n- **Hooks**: `before/after/failure/success`\n- **Capacity accounting** (quota)\n- **Graceful or fast stop** (`Drain` / `Stop`) and **restartable** queues (`Start()` after `Stop()`)\n\nError/Hook semantics:\n\n- `ErrStopEnvelope` — intentional stop of a specific envelope:\n    - the envelope is **forgotten**, **not** rescheduled;\n    - if raised in `beforeHook`/`invoke`, the `afterHook` still runs (within a time-bounded context); `successHook` does **not** run.\n- `context.Canceled` / `context.DeadlineExceeded` — not a success:\n    - envelope is forgotten; periodic ones are rescheduled, one-off ones are not.\n- Any other error:\n    - **periodic** → rescheduled (if queue is alive);\n    - **one-off** → defer to `failureHook` decision (`RetryNow` / `RetryAfter` / `Drop`).\n- Each hook runs with its own timeout: a fraction `frac=0.5` of the envelope's `deadline`, but at least `hardHookLimit` (800ms). Hook timeouts are derived from the task context `tctx`, so hooks never outlive the envelope deadline.\n\nConcurrency controls (brief):\n\n- `stateMu` guards the FSM state (RLock read / Lock write)\n- `lifecycleMu` serializes Start/Stop/queue swap\n- `queueMu` guards the inner workqueue pointer\n- `pendingMu` guards the pre-start buffer\n- `run` is an atomic fast flag for “queue alive”\n- Capacity accounting is atomic via `tryReserve/inc/dec/unreserve`\n\nOther highlights:\n\n- **Worker pool** via `WithLimitOption(n)`\n- **Start/Stop/Start**: tasks sent before first start are buffered and flushed on `Start()`\n- **Periodic vs one-off**: `interval \u003e 0` means periodic; `interval == 0` means one-off\n- **Deadlines**: `deadline \u003e 0` bounds `invoke` time via `context.WithTimeout` in the worker\n- **Stamps**: both global (queue-level) and per-envelope (task-level), with predictable execution order\n- **Panic safety**: panics inside task are handled (`Forget+Done`) and logged with stack; worker keeps running\n- **Prometheus metrics**: use `client-go` workqueue metrics\n\n---\n\n## Installation\n\n```bash\ngo get github.com/PavelAgarkov/rate-envelope-queue\n```\n\nRecommended pins (compatible with this package):\n\n```bash\ngo get k8s.io/client-go@v0.34.0\ngo get k8s.io/component-base@v0.34.0\n```\n\n**Requires:** Go **1.24+**.\n\n---\n\n## Quick Start\n\nSee full examples in [`examples/`](./examples):\n\n- `queue_with_simple_start_stop_dynamic_execute.go`\n- `simple_queue_with_simple_preset_envelopes.go`\n- `simple_queue_with_simple_schedule_envelopes.go`\n- `simple_queue_with_simple_dynamic_envelopes.go`\n- `simple_queue_with_simple_combine_envelopes.go`\n\nCapacity scenarios (accounting correctness):\n\n**Drain + waiting=true** — wait for all workers; all `dec()` happen; no remainder.\n```go\nenvelopeQueue := NewRateEnvelopeQueue(\n    parent,\n    \"test_queue\",\n    WithLimitOption(5),\n    WithWaitingOption(true),\n    WithStopModeOption(Drain),\n    WithAllowedCapacityOption(50),\n)\n```\n\n**Stop + waiting=true** — after `wg.Wait()` we subtract the “tail” (`cur - pend`), the counter converges.\n```go\nenvelopeQueue := NewRateEnvelopeQueue(\n    parent,\n    \"test_queue\",\n    WithLimitOption(5),\n    WithWaitingOption(true),\n    WithStopModeOption(Stop),\n    WithAllowedCapacityOption(50),\n)\n```\n\n**Unlimited capacity** — `WithAllowedCapacityOption(0)` removes admission limits; the `currentCapacity` metric still reflects actual occupancy.\n```go\nenvelopeQueue := NewRateEnvelopeQueue(\n    parent,\n    \"test_queue\",\n    WithLimitOption(5),\n    WithWaitingOption(true),\n    WithStopModeOption(Drain),\n    WithAllowedCapacityOption(0),\n)\n```\n\nMinimal API sketch:\n```go\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel()\n\nq := NewRateEnvelopeQueue(\n    ctx,\n    \"emails\",\n    WithLimitOption(4),\n    WithWaitingOption(true),\n    WithStopModeOption(Drain),\n    WithAllowedCapacityOption(1000),\n    WithStamps(LoggingStamp()),\n)\n\nemailOnce, _ := NewEnvelope(\n    WithId(1),\n    WithType(\"email\"),\n    WithScheduleModeInterval(0),          // one-off\n    WithDeadline(3*time.Second),\n    WithInvoke(func(ctx context.Context, e *Envelope) error { return nil }),\n)\n\nticker, _ := NewEnvelope(\n    WithId(2),\n    WithType(\"metrics\"),\n    WithScheduleModeInterval(5*time.Second), // periodic\n    WithDeadline(2*time.Second),\n    WithInvoke(func(ctx context.Context, e *Envelope) error { return nil }),\n)\n\nq.Start()\n_ = q.Send(emailOnce, ticker)\n// ...\nq.Stop()\nq.Start() // restart if needed\n```\n\n---\n\n## Concepts \u0026 Contracts\n\n### Envelope\n\n```go\ne, err := NewEnvelope(\n    WithId(123), // optional, for logs\n    WithType(\"my_task\"), // optional, for logs\n    WithScheduleModeInterval(time.Second), // 0 = one-off\n    WithDeadline(500*time.Millisecond),    // 0 = no deadline\n    WithBeforeHook(func(ctx context.Context, e *Envelope) error { return nil }),\n    WithInvoke(func(ctx context.Context, e *Envelope) error { return nil }), // required\n    WithAfterHook(func(ctx context.Context, e *Envelope) error { return nil }),\n    WithFailureHook(func(ctx context.Context, e *Envelope, err error) Decision {\n        return DefaultOnceDecision()               // Drop by default\n        // return RetryOnceAfterDecision(5 * time.Second)\n        // return RetryOnceNowDecision()\n    }),\n    WithSuccessHook(func(ctx context.Context, e *Envelope) {}),\n    WithStampsPerEnvelope(LoggingStamp()),\n    WithPayload(myPayload),\n)\n```\n\nValidation:\n- `invoke` is required; `interval \u003e= 0`; `deadline \u003e= 0`\n- For periodic: `deadline` **must not exceed** `interval` → `ErrAdditionEnvelopeToQueueBadIntervals`\n\nSpecial error:\n- `ErrStopEnvelope` — gracefully stops **this envelope only** (no reschedule)\n\n### Queue\n\n```go\nq := NewRateEnvelopeQueue(ctx, \"queue-name\",\n    WithLimitOption(n),\n    WithWaitingOption(true|false),\n    WithStopModeOption(Drain|Stop),\n    WithAllowedCapacityOption(cap),         // 0 = unlimited\n    WithWorkqueueConfigOption(conf),\n    WithLimiterOption(limiter),\n    WithStamps(stamps...),\n)\nq.Start()\nerr := q.Send(e1, e2, e3)                   // ErrAllowedQueueCapacityExceeded on overflow\nq.Stop()\n```\n\n**Pre-start buffer.** In `init`, `Send()` pushes envelopes into an internal buffer; on `Start()` they are flushed into the workqueue.\n\n### Stamps (middleware)\n\n```go\ntype (\n    Invoker  func(ctx context.Context, envelope *Envelope) error\n    Stamp    func(next Invoker) Invoker\n)\n```\n\nOrder: **global** stamps (outer) wrap **per-envelope** stamps (inner), then `Envelope.invoke`.\n\nA sample `LoggingStamp()` is provided for demonstration.\n\n---\n\n## Worker Behavior\n\n| Result / condition                      | Queue action                                                                         |\n|-----------------------------------------|--------------------------------------------------------------------------------------|\n| `invoke` returns `nil`                  | `Forget`; if `interval \u003e 0` and alive → `AddAfter(interval)`                         |\n| `context.Canceled` / `DeadlineExceeded` | `Forget`; if periodic and alive → `AddAfter(interval)`                               |\n| `ErrStopEnvelope`                       | `Forget`; **no** reschedule                                                          |\n| Error on **periodic**                   | `Forget`; if alive → `AddAfter(interval)`                                            |\n| Error on **one-off** + `failureHook`    | Use decision: `RetryNow` / `RetryAfter(d)` / `Drop`                                  |\n| Panic in task                           | `Forget + Done` + stack log; worker continues                                        |\n\n\u003e “Queue is alive” = `run == true`, state is `running`, base context not done, and `workqueue` not shutting down.\n\n---\n\n## Stop Modes\n\n| Waiting \\\\ StopMode | `Drain` (graceful)                              | `Stop` (fast)                                   |\n|---------------------|--------------------------------------------------|--------------------------------------------------|\n| `true`              | Wait for workers; `ShutDownWithDrain()`         | Wait for workers; `ShutDown()`                   |\n| `false`             | No wait; `ShutDownWithDrain()`                  | **Immediate** stop; `ShutDown()`                 |\n\nAfter `Stop()` you can call `Start()` again: a fresh inner `workqueue` will be created.\n\n---\n\n## Capacity Limiting\n\n`WithAllowedCapacityOption(cap)` limits the **total** number of in-flight/queued/delayed items (including reschedules).  \nIf the limit would be exceeded, `Send()` returns `ErrAllowedQueueCapacityExceeded`.  \n`currentCapacity` is updated on add, reschedule, and completion.\n\n- `cap == 0` → **unlimited** admission; the `currentCapacity` metric still tracks actual occupancy.\n- `Stop + waiting=false + StopMode=Stop` — documented tail leakage in accounting. Use `Drain` or `waiting=true` for accurate capacity convergence.\n\n---\n\n## Benchmarks\n\nCommand examples:\n\n```bash\ngo test -bench=BenchmarkQueueFull -benchmem\ngo test -bench=BenchmarkQueueInterval -benchmem\n```\n\nNumbers provided by the author (your CPU/env will vary):\n\n```\nBenchmarkQueueFull-8         3212882               348.7 ns/op            40 B/op          1 allocs/op\nBenchmarkQueueInterval-8      110313             12903 ns/op            1809 B/op         24 allocs/op\n```\n\n---\n\n## Metrics (Prometheus)\n\nWorkqueue metrics are enabled via blank import:\n\n```go\nimport (\n    _ \"k8s.io/component-base/metrics/prometheus/workqueue\"\n    \"k8s.io/component-base/metrics/legacyregistry\"\n    \"net/http\"\n)\n\nfunc serveMetrics() {\n    mux := http.NewServeMux()\n    mux.Handle(\"/metrics\", legacyregistry.Handler())\n    go http.ListenAndServe(\":8080\", mux)\n}\n```\n\nYour queue name (`QueueConfig.Name`) is included in workqueue metric labels (`workqueue_*`: adds, depth, work_duration, retries, etc.).\n\n---\n\n## Examples\n\nSee the [`examples/`](./examples) folder for runnable snippets covering one-off jobs, periodic schedules, combined modes, and dynamic dispatch.\n\n---\n\n## License\n\nMIT — see [`LICENSE`](./LICENSE).\n\n---\n\n---\n\n# rate-envelope-queue (Русская версия)\n\n\u003e Лёгкая, потокобезопасная обёртка над `k8s.io/client-go/util/workqueue` для управления задачами (**envelopes**) с фиксированным пулом воркеров, периодическим планированием, дедлайнами, хуками и **stamps (middleware)**. Добавляет безопасный жизненный цикл очереди (**Start/Stop/Start**), буферизацию задач до старта и ограничение ёмкости очереди.\n\n\u003e В основе — `workqueue.TypedRateLimitingInterface`. Дедупликация происходит по **указателю** на элемент: повторный `Add` того же *указателя* пока он в обработке — игнорируется.\n\n---\n\n## Содержание\n\n- [Ключевые возможности](#ключевые-возможности)\n- [Установка](#установка)\n- [Быстрый старт](#быстрый-старт)\n- [Концепции и контракты](#концепции-и-контракты)\n    - [Envelope](#envelope-1)\n    - [Очередь](#очередь)\n    - [Stamps (middleware)](#stamps-middleware-1)\n- [Поведение воркера](#поведение-воркера)\n- [Режимы остановки](#режимы-остановки)\n- [Ограничение ёмкости](#ограничение-ёмкости)\n- [Бенчмарки](#бенчмарки)\n- [Метрики (Prometheus)](#метрики-prometheus)\n- [Примеры](#примеры)\n- [Лицензия](#лицензия-1)\n\n---\n\n## Ключевые возможности\n\nПотокобезопасная локальная очередь в памяти приложения:\n\n- Все публичные методы безопасны при вызовах из нескольких горутин. `Send` можно вызывать параллельно. Воркеров может быть несколько (`limit`). Вызовы `Start/Stop` сериализуются внутри.\n- Это **не** распределённая очередь: гарантий между разными процессами/узлами нет. Код хуков/инвокеров должен сам обеспечивать потокобезопасность при доступе к общим ресурсам.\n\nЧто внутри:\n\n- Прозрачный автомат состояний: `init → running → stopping → stopped`\n- **Одноразовые и периодические** задачи\n- **Цепочка middleware** через `Stamp`\n- **Хуки**: `before/after/failure/success`\n- **Учёт ёмкости** (quota)\n- **Мягкий или быстрый останов** (`Drain` / `Stop`) и **повторный старт** (`Start()` после `Stop()`)\n\nСемантика ошибок и хуков:\n\n- `ErrStopEnvelope` — намеренная остановка конкретного конверта:\n    - конверт **забывается**, **не** перепланируется;\n    - если ошибка возникла в `beforeHook`/`invoke`, `afterHook` всё равно вызовется (с ограниченным временем); `successHook` **не** вызывается.\n- `context.Canceled` / `context.DeadlineExceeded` — это **не** успех:\n    - конверт забывается; периодический — перепланируется, одноразовый — нет.\n- Любая другая ошибка:\n    - **периодическая** → перепланируется (если очередь «жива»);\n    - **одноразовая** → решение через `failureHook` (`RetryNow` / `RetryAfter` / `Drop`).\n- Каждый хук выполняется с собственным таймаутом: доля `frac=0.5` от `deadline` конверта, но не меньше `hardHookLimit` (800мс). Таймауты «висят» на `tctx`, т.е. хуки никогда не переживут дедлайн конверта.\n\nПотокобезопасность (коротко):\n\n- `stateMu` — чтение/запись состояния\n- `lifecycleMu` — сериализация Start/Stop/смены очереди\n- `queueMu` — доступ к внутренней очереди\n- `pendingMu` — буфер задач до старта\n- `run` — атомарный флаг «жива ли очередь»\n- Учёт ёмкости — атомарные операции `tryReserve/inc/dec/unreserve`\n\nПрочее:\n\n- **Пул воркеров**: `WithLimitOption(n)`\n- **Start/Stop/Start**: задачи, добавленные до первого `Start()`, буферизуются и переливаются в очередь при старте\n- **Периодические / одноразовые**: `interval \u003e 0` — периодическая; `interval == 0` — одноразовая\n- **Дедлайны**: `deadline \u003e 0` ограничивает время `invoke` через `context.WithTimeout`\n- **Stamps**: глобальные и на уровне конверта, порядок выполнения предсказуем\n- **Защита от паник**: паника в задаче → `Forget+Done` и лог стека; воркер продолжает работу\n- **Метрики Prometheus**: из `client-go` workqueue\n\n---\n\n## Установка\n\n```bash\ngo get github.com/PavelAgarkov/rate-envelope-queue\n```\n\nРекомендуемые версии:\n\n```bash\ngo get k8s.io/client-go@v0.34.0\ngo get k8s.io/component-base@v0.34.0\n```\n\n**Требования:** Go **1.24+**.\n\n---\n\n## Быстрый старт\n\nСмотрите каталог [`examples/`](./examples):\n\n- `queue_with_simple_start_stop_dynamic_execute.go`\n- `simple_queue_with_simple_preset_envelopes.go`\n- `simple_queue_with_simple_schedule_envelopes.go`\n- `simple_queue_with_simple_dynamic_envelopes.go`\n- `simple_queue_with_simple_combine_envelopes.go`\n\nСценарии ёмкости (корректность учёта):\n\n**Drain + waiting=true** — дожидаемся всех воркеров; все `dec()` прошли; остатка нет.\n```go\nenvelopeQueue := NewRateEnvelopeQueue(\n    parent,\n    \"test_queue\",\n    WithLimitOption(5),\n    WithWaitingOption(true),\n    WithStopModeOption(Drain),\n    WithAllowedCapacityOption(50),\n)\n```\n\n**Stop + waiting=true** — после `wg.Wait()` снимается «хвост» (`cur - pend`), счётчик сходится.\n```go\nenvelopeQueue := NewRateEnvelopeQueue(\n    parent,\n    \"test_queue\",\n    WithLimitOption(5),\n    WithWaitingOption(true),\n    WithStopModeOption(Stop),\n    WithAllowedCapacityOption(50),\n)\n```\n\n**Безлимитная ёмкость** — `WithAllowedCapacityOption(0)` убирает ограничение приёма; метрика `currentCapacity` отражает фактическую занятость.\n```go\nenvelopeQueue := NewRateEnvelopeQueue(\n    parent,\n    \"test_queue\",\n    WithLimitOption(5),\n    WithWaitingOption(true),\n    WithStopModeOption(Drain),\n    WithAllowedCapacityOption(0),\n)\n```\n\nМини‑пример:\n\n```go\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel()\n\nq := NewRateEnvelopeQueue(\n    ctx,\n    \"emails\",\n    WithLimitOption(4),\n    WithWaitingOption(true),\n    WithStopModeOption(Drain),\n    WithAllowedCapacityOption(1000),\n    WithStamps(LoggingStamp()),\n)\n\nemailOnce, _ := NewEnvelope(\n    WithId(1),\n    WithType(\"email\"),\n    WithScheduleModeInterval(0),          // одноразовая\n    WithDeadline(3*time.Second),\n    WithInvoke(func(ctx context.Context, e *Envelope) error { return nil }),\n)\n\nticker, _ := NewEnvelope(\n    WithId(2),\n    WithType(\"metrics\"),\n    WithScheduleModeInterval(5*time.Second), // периодическая\n    WithDeadline(2*time.Second),\n    WithInvoke(func(ctx context.Context, e *Envelope) error { return nil }),\n)\n\nq.Start()\n_ = q.Send(emailOnce, ticker)\n// ...\nq.Stop()\nq.Start() // при необходимости можно снова стартовать\n```\n\n---\n\n## Концепции и контракты\n\n### Envelope\n\n```go\ne, err := NewEnvelope(\n    WithId(123),                   // опционально, для логов\n    WithType(\"my_task\"),           // опционально, для логов\n    WithScheduleModeInterval(time.Second), // 0 = одноразовая\n    WithDeadline(500*time.Millisecond),    // 0 = без дедлайна\n    WithBeforeHook(func(ctx context.Context, e *Envelope) error { return nil }),\n    WithInvoke(func(ctx context.Context, e *Envelope) error { return nil }), // обязательно\n    WithAfterHook(func(ctx context.Context, e *Envelope) error { return nil }),\n    WithFailureHook(func(ctx context.Context, e *Envelope, err error) Decision {\n        return DefaultOnceDecision()               // по умолчанию Drop\n        // return RetryOnceAfterDecision(5 * time.Second)\n        // return RetryOnceNowDecision()\n    }),\n    WithSuccessHook(func(ctx context.Context, e *Envelope) {}),\n    WithStampsPerEnvelope(LoggingStamp()),\n    WithPayload(myPayload),\n)\n```\n\nВалидация:\n- `invoke` обязателен; `interval \u003e= 0`; `deadline \u003e= 0`\n- Для периодических: `deadline` **не должен превышать** `interval` → `ErrAdditionEnvelopeToQueueBadIntervals`\n\nСпец‑ошибка:\n- `ErrStopEnvelope` — корректно прекращает **только этот конверт** (без перепланирования)\n\n### Очередь\n\n```go\nq := NewRateEnvelopeQueue(ctx, \"queue-name\",\n    WithLimitOption(n),\n    WithWaitingOption(true|false),\n    WithStopModeOption(Drain|Stop),\n    WithAllowedCapacityOption(cap),         // 0 = без лимита\n    WithWorkqueueConfigOption(conf),\n    WithLimiterOption(limiter),\n    WithStamps(stamps...),\n)\nq.Start()\nerr := q.Send(e1, e2, e3)                   // ErrAllowedQueueCapacityExceeded при переполнении\nq.Stop()\n```\n\n**Буфер до старта.** В состоянии `init` `Send()` складывает задачи во внутренний буфер; при `Start()` — они переливаются в `workqueue`.\n\n### Stamps (middleware)\n\n```go\ntype (\n    Invoker  func(ctx context.Context, envelope *Envelope) error\n    Stamp    func(next Invoker) Invoker\n)\n```\n\nПорядок: **сначала глобальные** stamps (внешние), затем **per‑envelope** (внутренние), после — `Envelope.invoke`.\n\n`LoggingStamp()` — пример для иллюстрации (не «серебряная пуля» для продакшена).\n\n---\n\n## Поведение воркера\n\n| Событие / результат                     | Действие очереди                                                                     |\n|-----------------------------------------|--------------------------------------------------------------------------------------|\n| `invoke` вернул `nil`                   | `Forget`; если `interval \u003e 0` и очередь «жива» → `AddAfter(interval)`                |\n| `context.Canceled` / `DeadlineExceeded` | `Forget`; если периодическая и очередь «жива» → `AddAfter(interval)`                 |\n| `ErrStopEnvelope`                       | `Forget`; **не** перепланируем                                                       |\n| Ошибка у **периодической**              | `Forget`; если очередь «жива» → `AddAfter(interval)`                                 |\n| Ошибка у **одноразовой** + `failureHook`| Решение пользователя: `RetryNow` / `RetryAfter(d)` / `Drop`                          |\n| Паника в задаче                         | `Forget + Done` + лог стека; воркер продолжает работу                                |\n\n\u003e «Очередь жива» = `run == true`, `state == running`, базовый контекст не завершён и `workqueue` не в shutdown.\n\n---\n\n## Режимы остановки\n\n| Waiting \\\\ StopMode | `Drain` (мягкая)                                   | `Stop` (жёсткая)                           |\n|---------------------|-----------------------------------------------------|--------------------------------------------|\n| `true`              | Ждать воркеров; `ShutDownWithDrain()`               | Ждать воркеров; `ShutDown()`               |\n| `false`             | Без ожидания воркеров; `ShutDownWithDrain()`        | **Мгновенный** останов; `ShutDown()`       |\n\nПосле `Stop()` можно вызывать `Start()` повторно: создаётся новый внутренний `workqueue`.\n\n---\n\n## Ограничение ёмкости\n\n`WithAllowedCapacityOption(cap)` ограничивает суммарное число элементов в системе (включая перепланированные).  \nПри попытке превышения лимита `Send()` возвращает `ErrAllowedQueueCapacityExceeded`.  \n`currentCapacity` обновляется при добавлении, перепланировании и завершении обработки.\n\n- `cap == 0` → **безлимит** по приёму; метрика `currentCapacity` отражает фактическую занятость.\n- `Stop + waiting=false + StopMode=Stop` — документированная утечка «хвоста» в учёте. Для точной сходимости используйте `Drain` или `waiting=true`.\n\n---\n\n## Бенчмарки\n\nКак запускать:\n\n```bash\ngo test -bench=BenchmarkQueueFull -benchmem\ngo test -bench=BenchmarkQueueInterval -benchmem\n```\n\nЦифры автора (зависят от CPU/окружения):\n\n```\nBenchmarkQueueFull-8         3212882               348.7 ns/op            40 B/op          1 allocs/op\nBenchmarkQueueInterval-8      110313             12903 ns/op            1809 B/op         24 allocs/op\n```\n\n---\n\n## Метрики (Prometheus)\n\nМетрики `workqueue` активируются бланк‑импортом:\n\n```go\nimport (\n    _ \"k8s.io/component-base/metrics/prometheus/workqueue\"\n    \"k8s.io/component-base/metrics/legacyregistry\"\n    \"net/http\"\n)\n\nfunc serveMetrics() {\n    mux := http.NewServeMux()\n    mux.Handle(\"/metrics\", legacyregistry.Handler())\n    go http.ListenAndServe(\":8080\", mux)\n}\n```\n\nИмя очереди (`QueueConfig.Name`) попадает в лейблы метрик (`workqueue_*`: adds, depth, work_duration, retries и т.д.).\n\n---\n\n## Примеры\n\nСмотрите каталог [`examples/`](./examples) — там есть готовые варианты для одноразовых задач, периодических расписаний, комбинированных сценариев и динамического диспатча.\n\n---\n\n## Лицензия\n\nMIT — см. [`LICENSE`](./LICENSE).","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimplegear%2Frate-envelope-queue","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimplegear%2Frate-envelope-queue","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimplegear%2Frate-envelope-queue/lists"}