Full Code of robacarp/mosquito for AI

master 6ededc5e7a34 cached
134 files
306.2 KB
83.0k tokens
1 requests
Download .txt
Showing preview only (338K chars total). Download the full file or copy to clipboard to get everything.
Repository: robacarp/mosquito
Branch: master
Commit: 6ededc5e7a34
Files: 134
Total size: 306.2 KB

Directory structure:
gitextract_f3v7pvaw/

├── .claude/
│   ├── hooks/
│   │   └── session-start.sh
│   ├── settings.json
│   └── todo.md
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   └── bug.md
│   └── workflows/
│       ├── ci.yml
│       └── docs.yml
├── .gitignore
├── .tool-versions
├── :w
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── benchmark/
│   ├── benchmark.cr
│   └── jobs/
│       └── emit_message_job.cr
├── demo/
│   ├── jobs/
│   │   ├── custom_serializers.cr
│   │   ├── periodically_puts.cr
│   │   ├── queued_job.cr
│   │   ├── rate_limited_job.cr
│   │   └── unique_job.cr
│   └── run.cr
├── scripts/
│   ├── increment_version
│   ├── lib/
│   │   └── increment_version.sh
│   └── version_tag
├── shard.yml
├── spec/
│   ├── helpers/
│   │   ├── bare_base_class.cr
│   │   ├── configuration_helper.cr
│   │   ├── global_helpers.cr
│   │   ├── logging_helper.cr
│   │   ├── mock_coordinator.cr
│   │   ├── mock_executor.cr
│   │   ├── mock_overseer.cr
│   │   ├── mock_queue_list.cr
│   │   ├── mocks.cr
│   │   ├── null_dequeue_adapter.cr
│   │   ├── pub_sub.cr
│   │   └── spy_dequeue_adapter.cr
│   ├── mosquito/
│   │   ├── api/
│   │   │   ├── executor_config_spec.cr
│   │   │   ├── executor_spec.cr
│   │   │   ├── job_run_spec.cr
│   │   │   ├── overseer_spec.cr
│   │   │   ├── periodic_job_spec.cr
│   │   │   ├── publisher_spec.cr
│   │   │   └── queue_spec.cr
│   │   ├── api_spec.cr
│   │   ├── backend/
│   │   │   ├── deleting_spec.cr
│   │   │   ├── executor_spec.cr
│   │   │   ├── expiring_list_spec.cr
│   │   │   ├── hash_storage_spec.cr
│   │   │   ├── inspection_spec.cr
│   │   │   ├── lock_spec.cr
│   │   │   ├── overseer_spec.cr
│   │   │   └── queueing_spec.cr
│   │   ├── backend_spec.cr
│   │   ├── base_spec.cr
│   │   ├── configuration_spec.cr
│   │   ├── dequeue_adapters/
│   │   │   ├── concurrency_limited_dequeue_adapter_spec.cr
│   │   │   ├── remote_config_dequeue_adapter_spec.cr
│   │   │   ├── shuffle_dequeue_adapter_spec.cr
│   │   │   └── weighted_dequeue_adapter_spec.cr
│   │   ├── exceptions_spec.cr
│   │   ├── job/
│   │   │   └── job_state_spec.cr
│   │   ├── job_run/
│   │   │   ├── rescheduling_spec.cr
│   │   │   ├── running_spec.cr
│   │   │   └── storage_spec.cr
│   │   ├── job_run_spec.cr
│   │   ├── job_spec.cr
│   │   ├── key_builder_spec.cr
│   │   ├── metadata_spec.cr
│   │   ├── periodic_job_run_spec.cr
│   │   ├── periodic_job_spec.cr
│   │   ├── queue_spec.cr
│   │   ├── queued_job_spec.cr
│   │   ├── rate_limiter_spec.cr
│   │   ├── resource_gate_spec.cr
│   │   ├── runnable_spec.cr
│   │   ├── runners/
│   │   │   ├── coordinator_spec.cr
│   │   │   ├── executor_spec.cr
│   │   │   ├── overseer_spec.cr
│   │   │   ├── queue_list_spec.cr
│   │   │   └── run_at_most_spec.cr
│   │   ├── serializers/
│   │   │   └── primitive_serializers_spec.cr
│   │   ├── testing_backend_spec.cr
│   │   ├── unique_job_spec.cr
│   │   └── version_spec.cr
│   └── spec_helper.cr
└── src/
    ├── mosquito/
    │   ├── api/
    │   │   ├── concurrency_config.cr
    │   │   ├── executor.cr
    │   │   ├── executor_config.cr
    │   │   ├── job_run.cr
    │   │   ├── observability/
    │   │   │   └── publisher.cr
    │   │   ├── overseer.cr
    │   │   ├── periodic_job.cr
    │   │   ├── queue.cr
    │   │   └── queue_list.cr
    │   ├── api.cr
    │   ├── backend.cr
    │   ├── base.cr
    │   ├── configuration.cr
    │   ├── dequeue_adapter.cr
    │   ├── dequeue_adapters/
    │   │   ├── concurrency_limited_dequeue_adapter.cr
    │   │   ├── remote_config_dequeue_adapter.cr
    │   │   ├── shuffle_dequeue_adapter.cr
    │   │   └── weighted_dequeue_adapter.cr
    │   ├── exceptions.cr
    │   ├── gates/
    │   │   ├── open_gate.cr
    │   │   └── threshold_gate.cr
    │   ├── job.cr
    │   ├── job_run.cr
    │   ├── key_builder.cr
    │   ├── metadata.cr
    │   ├── periodic_job.cr
    │   ├── periodic_job_run.cr
    │   ├── queue.cr
    │   ├── queued_job.cr
    │   ├── rate_limiter.cr
    │   ├── redis_backend.cr
    │   ├── resource_gate.cr
    │   ├── runnable.cr
    │   ├── runner.cr
    │   ├── runners/
    │   │   ├── coordinator.cr
    │   │   ├── executor.cr
    │   │   ├── idle_wait.cr
    │   │   ├── overseer.cr
    │   │   ├── queue_list.cr
    │   │   └── run_at_most.cr
    │   ├── scheduled_job.cr
    │   ├── serializers/
    │   │   └── primitives.cr
    │   ├── test_backend.cr
    │   ├── unique_job.cr
    │   └── version.cr
    ├── mosquito.cr
    └── ye_olde_redis.cr

================================================
FILE CONTENTS
================================================

================================================
FILE: .claude/hooks/session-start.sh
================================================
#!/bin/bash
set -euo pipefail

# Only run in remote (cloud) environments
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
  exit 0
fi

echo '{"async": true, "asyncTimeout": 300000}'

# Read Crystal version from .tool-versions
CRYSTAL_VERSION=$(grep '^crystal ' "$CLAUDE_PROJECT_DIR/.tool-versions" | awk '{print $2}')

# Install Crystal compiler if not already present
if ! command -v crystal &> /dev/null; then
  # Install system dependencies required by Crystal
  apt-get update
  apt-get install -y libgmp-dev libxml2-dev libevent-dev libgc-dev

  # Download and install Crystal from GitHub releases
  curl -fsSL "https://github.com/crystal-lang/crystal/releases/download/${CRYSTAL_VERSION}/crystal-${CRYSTAL_VERSION}-1-linux-x86_64-bundled.tar.gz" -o /tmp/crystal.tar.gz
  mkdir -p /usr/local/crystal
  tar -xzf /tmp/crystal.tar.gz -C /usr/local/crystal --strip-components=2
  ln -sf /usr/local/crystal/bin/crystal /usr/local/bin/crystal
  ln -sf /usr/local/crystal/bin/shards /usr/local/bin/shards
  rm /tmp/crystal.tar.gz
fi

# Start Redis server if not already running
if ! redis-cli ping &> /dev/null 2>&1; then
  redis-server --daemonize yes
fi

# Disable RDB persistence to avoid dump.rdb noise in the project directory
redis-cli config set save "" > /dev/null 2>&1

# Install Crystal shard dependencies
cd "$CLAUDE_PROJECT_DIR"
shards install


================================================
FILE: .claude/settings.json
================================================
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
          }
        ]
      }
    ]
  }
}


================================================
FILE: .claude/todo.md
================================================
# Migration from publish_metrics branch

## Background

The `publish_metrics` branch contains observability improvements that need to be migrated to master.
This branch has ~24 commits of work dating back to October 2024.

## Functionality to Migrate

Items ordered by size of change, smallest first.

### 1. Metadata Self-Cleanup ✅
Add TTL to metadata so stale entries auto-expire.

- [x] Add `@metadata.delete in: 1.hour` to Executor heartbeat
- [x] Add `@metadata.delete in: 1.hour` to Overseer heartbeat

Already implemented - `Metadata#heartbeat!` includes `delete in: 1.hour` and both observers use it.

### 2. Overseer Event Naming Standardization ✅
Standardize to past tense for consistency with other events.

- [x] "starting" → "started"
- [x] "stopping" → "stopped"
- [x] "stopped" → "exited"

Done in `src/mosquito/api/overseer.cr`.

### 3. Executor Bug Fix ✅
- [x] Fix latent bug: executor calculating run time incorrectly (see commit `mvouzzrz`)

Fixed `100_000` → `1_000_000` in microseconds calculation in `src/mosquito/api/executor.cr`.

### 4. Stable Instance IDs — Skipped
`object_id` is sufficient; no need for `Random::Secure.hex` IDs.

### 5. Nested Publish Context ✅
Allow executor events to be namespaced under their parent overseer.

- [x] Add parent context support to `PublishContext` initializer
- [x] Pass overseer reference to Executor
- [x] Update Executor observer to create PublishContext with overseer as parent
- [x] Executor events publish under `[:overseer, overseer_id, :executor, executor_id]`
- [x] Fix tests (executor/overseer specs, mock_overseer)

Done.

### 6. Observability Gating ✅
Gate metadata writes behind existing `publish_metrics` config.

- [x] Gate `heartbeat!` in Executor observer behind `metrics` macro
- [x] Gate `heartbeat` in Overseer observer behind `metrics` macro (includes `register_overseer`)
- [x] Gate `update_executor_list` in Overseer observer behind `metrics` macro
- [x] Fix pre-existing race condition in executor spec (lazy getter initialization across fibers)

Decided against a separate `Enabled` module / `enable_observability` config — no compelling reason
to have two flags. Reused the existing `metrics` macro which checks `publish_metrics`.

### 7. Observability Tests ✅

#### Fix `assert_message_received` ✅
The helper in `spec/helpers/pub_sub.cr` doesn't actually assert — `find` returns nil
and the result is discarded. All existing event publishing tests are vacuous (always pass).
- [x] Fix `assert_message_received` to fail when no matching message is found
- [x] Fix overseer event assertions to match actual event names

#### Metrics gating ✅
- [x] Executor: heartbeat is skipped when `publish_metrics = false`
- [x] Event publishing is skipped when `publish_metrics = false` (tested via publisher_spec, covers all observers)

#### Queue observer events ✅
- [x] Publishes "rescheduled" event
- [x] Publishes "forgotten" event
- [x] Publishes "banished" event

#### Publish context structure ✅
- [x] Executor publish context is nested under overseer's context
- [x] Overseer publish context has correct originator key
- [x] Queue publish context has correct originator key

## Files to Reference on publish_metrics

Key source files:
- `src/mosquito/observability/concerns/enabled.cr`
- `src/mosquito/observability/concerns/publish_context.cr`
- `src/mosquito/observability/concerns/publisher.cr`
- `src/mosquito/observability/executor.cr`
- `src/mosquito/observability/overseer.cr`
- `src/mosquito/observability/queue.cr`

Key test files:
- `test/mosquito/observability/enabled_test.cr`
- `test/mosquito/observability/executor_test.cr`
- `test/mosquito/observability/overseer_test.cr`
- `test/mosquito/observability/queue_test.cr`

## Notes

- The publish_metrics branch has diverged (shown as `??` in jj) - resolve carefully
- Current working copy already has queue observer events (rescheduled, forgotten, banished)
- Duration averaging and expected_duration_ms already implemented on master
- Test directory structure (`test/` instead of `spec/`) already migrated on master


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: robacarp
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel


================================================
FILE: .github/ISSUE_TEMPLATE/bug.md
================================================
---
name: Bug
about: Mosquito has a bug!
title: ''
labels: ''
assignees: robacarp

---

Please include some details:

Crystal version: 0.28.0
Mosquito Shard version: 0.4.0


================================================
FILE: .github/workflows/ci.yml
================================================
name: Test and Demo
on:
  pull_request:
    branches:
      - master
  push:
    branches:
      - master

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        crystal_version: [1.19, latest]
        experimental:
          - false
        include:
          - crystal_version: nightly
            experimental: true

    name: Build
    runs-on: ubuntu-latest

    container:
      image: crystallang/crystal:latest

    continue-on-error: ${{ matrix.experimental }}
    services:
      redis:
        image: redis

    env:
      REDIS_URL: redis://redis:6379/1

    steps:
    - uses: actions/checkout@v4
    - run: apt-get update
    - uses: crystal-lang/install-crystal@v1
      with:
        crystal: ${{matrix.crystal_version}}
    - run: printenv
    - run: crystal --version
    - run: shards install
    - run: make test
    - run: make demo


================================================
FILE: .github/workflows/docs.yml
================================================
name: Build Docs
on:
  push:
    branches:
      - master

jobs:
  deploy:
    name: Running Docs
    runs-on: ubuntu-latest

    container:
      image: crystallang/crystal:latest

    steps:
    - uses: actions/checkout@v2
    - run: apt-get update
    - uses: crystal-lang/install-crystal@v1
    - run: crystal --version
    - run: shards install
    - run: crystal docs

    - name: Deploy
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./docs


================================================
FILE: .gitignore
================================================
/lib/
/bin/
/.shards/

# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
/shard.lock
docs
# Claude Code local user config (not hooks/settings which are shared)
.claude/local/
CLAUD.local.md


================================================
FILE: .tool-versions
================================================
crystal 1.19.1


================================================
FILE: :w
================================================



================================================
FILE: CHANGELOG.md
================================================
# Changelog

The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Mosquito::Api now allows for inspecting the state of the state of a mosquito cluster. Many of these features are disabled by default by the configuration property `config.publish_metrics`.
    - Executor api implemented in #147
    - JobRun api implemented in #148 and #161
    - Overseer api implemented in #150
    - Queue api implemented in #153
- Mosquito now publishes a variety of events and metrics to a redis pubsub channel. This behavior is disabled by default with the configuration property `config.publish_metrics`.
    - Executor events in #154: job-started and job-finished
    - Overseer events in #160: starting, executor-created, executor-died, stopping, and stopped
    - Queue events: enqueue, dequeue, reschedule, forget, and banish
    - Expected job duration is now published with executor events
  The Mosquito API can be used to subscribe to these events with `Mosquito::API.event_receiver`
- Pluggable dequeue adapters allow customizing how jobs are selected from queues (#183)
    - `DequeueAdapter` abstract base class defines the adapter interface
    - `ShuffleDequeueAdapter` is the default, preserving existing randomized behavior
    - `WeightedDequeueAdapter` allows queue-level prioritization via configurable weights
    - Configurable via `Mosquito.configure { |c| c.dequeue_adapter = ... }`
- Executor count is now configurable (default increased from 3 to 6) (#184)
    - Set via `Mosquito.configure { |c| c.executor_count = 10 }`
    - Override with the `MOSQUITO_EXECUTOR_COUNT` environment variable
- `JobRun#started_at` and `JobRun#finished_at` timestamps are now exposed as typed `Time?` getters (#179)
- Graceful worker shutdown: on SIGTERM/SIGINT the overseer stops dequeuing, waits for in-flight executors to finish, and requeues any jobs left in pending back to waiting (#190)
- Queues can now be paused and resumed. While paused, `#dequeue` returns nil and jobs accumulate until the queue is resumed. An optional duration enables automatic resumption, useful for backing off rate-limited resources. (#192)
- Overseers now take ownership of job runs when dequeued, and clean up abandoned pending job runs on startup (#180)
- Mosquito can now accept pre-existing backend connections via `Configuration#backend_connection`. This allows sharing a connection pool with the rest of an application. (#193)
- JobRun now uses Metadata for all backend storage operations, replacing direct backend calls with the Metadata abstraction layer.
- `Mosquito::UniqueJob` module provides opt-in job deduplication. Including the module in a job class prevents enqueueing duplicate jobs when an identical job is already waiting or scheduled. Uniqueness keys are derived from job parameters at compile time.

### Changed
- (breaking) `Configuration#connection_string` has been renamed to `Configuration#backend_connection_string` (#193)
- (minor breaking) Logs are now emitted from runners with a slighly different source tag. (#152)
  For example:
  The overseer boot message used to be:
    `INFO - mosquito.runners.overseer.4315742080: Overseer<4315742080> is starting`
  Now the message is simply:
    `INFO - mosquito.overseer: starting`
- Mosquito now runs CI checks for compatibility with Crystal 1.6
- The coordinator now uses UTC time instead of monotonic time

### Fixed
- Fixed a KeyError crash in the demo when job metadata was missing by using safe key access.
- the queue_list runner was never being shut down, but it is now as of (#165)
- Fixed a bug which would cause a mosquito server to hang at exit indefinitely if a job was mid-run during an interrupt. (#165)
- Fixed a bug which would cause a correctly exiting server to prematurely exit without emitting shutdown sequence logs and events. (#165)
- Crashed executors are now properly detected and replaced, preventing overseers from running with no executors
- Overseer now correctly deregisters on clean exit
- Pubsub logging now uses the `mosquito.events` namespace instead of the root `mosquito` namespace
- Queue `@empty` latch no longer permanently prevents re-dequeue after a queue drains
- Observer functionality is correctly gated behind the `publish_metrics` config flag
- Executor events are correctly scoped to within the overseer
- Fixed a latent bug which caused job duration to be reported incorrectly
- Fixed `Mosquito::Api.list_queues`

### Performance
- Optimized `metadata#set` to decrease the number of redis commands

## [2.0.0]
### Added
- Adds a test backend, which can be used to inspect jobs that were enqueued and
  the parameters they were enqueued with.
- Job#fail now takes an optional `retry` parameter which defaults to true, allowing
  a developer to explicitly mark a job as not retry-able during a job run. Additionally
  a `should_retry` property exists which can be set as well.
- Mosquito::Configuration now provides `global_prefix` to change the global Redis namespace 
  prefix, allowing for more than one mosquito app to share a redis instance (thanks @dammer, cf #134).

### Fixed
- PeriodicJobs are now correctly run once per interval in an environment with many workers.
- Running more than ~10 workers no longer causes workers to crash, fixing #137 (cf #138).
- Mosquito is now more broadly compatible with jgaskins redis, swapping 0.7.0 for 0.7, and
  forward compatible through 0.8. (thanks @rmarronnier)
- Mosquito now more gracefully responds to SIGTERM, fixes #122, cf #123.
- High CPU usage on linux is no longer an issue, fixes #126, cf #128.

### Breaking Changes
- The QueuedJob `params` macro has been replaced with `param`
  which declares only one parameter at a time.
- JobRun#delete now explicitly takes an Int, rather than simply defaulting to 0 (thanks @jwoertink, cf #136).
- removes deprecated Backend.delete(String, Int32), use Backend.delete(String, Int64) instead.
- removes deprecated Queue#length, use Queue#size instead.
- removes option to run the cron scheduler declaratively, it is now always on with a distributed lock.

### Performance
- Dramatically decreases the time spent listing queues #120
- Replaces #keys with #scan_each to list runners #138
- Provides for multiple executors operating under a single runner #123


## [1.0.2]
### Fixed
- Mosquito::Runner.start now captures the thread with a spin lock again. The new
  behavior of returning imediately can be achieved by calling #start(spin: false)   

## [1.0.1] [YANKED]
### Added
- Implements a distributed lock for scheduler coordination. The behavior is opt-in
  for now, but will become the default in the next release. See #108.
- Provides a helpful error message for most implementation errors dealing with
  declaring params.

### Changed
- Mosquito::QueuedJob: the `params` macro has been deprecated in favor of `param`.
  See #110.
- The deprecated Redis command [`rpoplpush`](https://redis.io/commands/rpoplpush/)
  is no longer used. This lifts the minimum redis server requirement up to 6.2.0
  and jgaskins redis to > 0.7.0.
- Mosquito::Runner.start no longer captures the thread with a spin lock. [DEFECT]

### Removed
- Mosquito config option `run_cron_scheduler` is no longer present, multiple
  workers will compete for a distributed lock instead. See #108.

## [1.0.0]
### Added
- Jobs can now specify their retry/reschedule logic with the #rescheduleable?
  and #reschedule_interval methods.
- Job metadata storage engine.
- Jobs can now specify `after` hooks.
- Mosquito::Runner now has a `stop` method which halts the runner after
  completion of any running tasks. See issue #21 and pull #87.
- Mosquito config option `run_cron_scheduler` is no longer present, multiple
  workers will compete for a distributed lock instead.

### Changed
- The storage backend is now implemented via interface, allowing alternate
  backends to be implemented.
- The rate limiting functionality is now implemented in a module,
  `Mosquito::RateLimiter`. See pull #77 for migration details.
- ** BREAKING ** `Job.job_type` has been replaced with `Job.queue_name`. The
  functionailty is identical but easier to access. See #86.
- `log` statements now properly identify where they're coming from rather than
  just 'mosquito'. See issue #78 and pull #88.
- Mosquito now connects to Redis using a connection pool. See #89
- ** BREAKING **  `Mosquito.settings` is now `Mosquito.configuration`. While
  this is technically a public API, it's unlikely anyone is using it for
  anything.
- Mosquito::Runner.start need not be called from a spawn, it will spawn on it's own.

### Removed
- Runner.idle_wait configuration is deprecated. Instead use
  Mosquito.configure#idle_wait.
- Built in serializer for Granite models, and the Model type alias. See
  Serializers in the documentation if the functionality is necessary.
- Mosquito no longer depends on luckyframework/habitat.

### Fixed
- Boolean false can now be specified as the default value for a parameter:
  `params(name = false)`

## [0.11.2] - 2022-01-25
### Fixed
- #66 Jobs with no parameters can now be enqueued without specifying an empty
  `params()`.
- #65 PeriodicJobs can now specify their run period in months.

### Notes
The v0 major version is now bugfix-only. Please update to v1.0. v0 will be
supported as long as it's feasible to do so.

## [0.11.1] - 2022-01-17
### Added
- Jobs can now specify `before` hooks, which can abort before the perform is
  triggered.
- The Cron scheduler for periodic jobs can now be disabled via
  Mosquito.configure#run_cron_scheduler
- The list of queues which are watched by the runner can now be configured via
  Mosquito.configure#run_from.

### Updated
- Redis shard 2.8.0, removes hash shims which are no longer needed. Thanks
  @jwoertink.

## [0.11.0] - 2021-04-10
Proforma release for Crystal 1.0.

## [0.10.0] - 2021-02-15
### Added
- UUID serializer helpers.

### Updated
- Switches from Benchmark.measure to Time.measure, thanks @anapsix.
- Runner.idle_wait is now configured via Mosquito.configure instead of directly
  on Mosquito::Runner.

## [0.9.0] - 2020-10-26
### Added
- Allows redis connection string to be specified via config option, thanks
  @watzon.

### Deprecated
- Connecting to redis via implicit REDIS_URL parameter is deprecated, thanks
  @watzon.

## [0.8.0] - 2020-05-28
### Fixed
- (Breaking) Dead tasks which have failed and expired are now cleaned up with a
  Redis TTL. See Pull #48.

## [0.7.0] - 2020-05-05
### Added
- ability to configure Runner.idle_wait period, thanks @mamantoha.

### Updated
- Point to Crystal 0.34.0, thanks @alex-lairan.

### Changed
- Replaces `Logger` with the more flexible `Log`.

## [0.6.0] - 2019-12-19
### Updated
- Point to Crystal 0.31.1, 0.32.1.
- Redis version, thanks @nsuchy.

## [0.5.0] - 2019-06-14
### Fixed
- Issue #26 Unresolved local var error, thanks @blacksmoke16.

## [0.4.0] - 2019-04-26
### Added
- Throttling logic, thanks @blacksmoke16.

## [0.3.0] - 2018-11-25
### Updated
- Point to crystal 0.27, thanks @blacksmoke16.

### Fixed
- Brittle/intermittently failing tests.

## [0.2.1] - 2018-10-01

### Added
- Logo, contributed by @psikoz.
- configuration for CI : `make test demo` will run all acceptance criteria.
- demo section.
- makefile.

### Updated
- specify crystal 0.26.
- simplify macro logic in QueuedJob.

## [0.2.0] - 2018-06-22
### Updated
- Specify crystal-redis 2.0 and crystal 0.25.

## [0.1.1] - 2018-06-08

### Added
- Job classes can now disable rescheduling on failure.

### Updated
- Readme.
- Misc typo fixes and flexibility upgrades.
- Update Crystal specification 0.23.1 -> .24.2.
- Correctly specify and sync version numbers from shard.yml / version.cr / git
  tag.
- Use configurable Logger instead of writing directly to stdout.
- Log output is now colorized and formatted to be read by human eyes.

### Changed
- Breaking: Update Mosquito::Model type alias to match updates to Granite.

### Fixed
- BUG: task id was mutating on each save, causing weird logging when tasks
  reschedule.
- PERFORMANCE: adding IDLE_WAIT to prevent slamming redis when the queues are
  empty. Smarter querying of the queues for work.


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2019 Robert L Carpenter

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.


================================================
FILE: Makefile
================================================
SHELL=/bin/bash

.PHONY: all
all: test
	shards build


.PHONY: test
test:
	crystal spec --error-trace -- --chaos

.PHONY: demo
demo:
	crystal run demo/run.cr --error-trace


================================================
FILE: README.md
================================================
<img src="logo/logotype_horizontal.svg" alt="mosquito">

[![GitHub](https://img.shields.io/github/license/mosquito-cr/mosquito.svg?style=for-the-badge)](https://tldrlegal.com/license/mit-license)

<img src="https://mosquito-cr.github.io/images/amber-mosquito-small.png" align="right">

Mosquito is a generic background job runner written primarily for Crystal. Significant inspiration from experience with the successes and failings many Ruby gems in this vein. Once compiled, a mosquito binary can start work in about 10 milliseconds.

Mosquito currently provides these features:

- Delayed execution (`SendEmailJob.new(email: :welcome, address: user.email).enqueue(in: 3.minutes)`)
- Scheduled / Periodic execution (`RunEveryHourJob.new`)
- Job Storage in Redis
- Automatic rescheduling of failed jobs
- Progressively increasing delay of rescheduled failed jobs
- Dead letter queue of jobs which have failed too many times
- Rate limited jobs

Current Limitations:
- Visibility into a running job network and queue is limited. There is a working proof of concept [visualization API](https://github.com/mosquito-cr/mosquito/issues/90) and [bare-bones terminal application](https://github.com/mosquito-cr/tui-visualizer).

## Project State

The Mosquito project is stable. A few folks are using Mosquito in production, and it's going well.

There are some features which would be nice to have, but what is here is both tried and tested.

If you're using Mosquito, please [get in touch](https://github.com/mosquito-cr/mosquito/discussions) on the Discussion board or [on Crystal chat](https://crystal-lang.org/community/) with any questions, feature suggestions, or feedback.

## Installation

Update your `shard.yml` to include mosquito:

```diff
dependencies:
+  mosquito:
+    github: mosquito-cr/mosquito
```

## Usage

### Step 1: Define a queued job

```crystal
# src/jobs/puts_job.cr
class PutsJob < Mosquito::QueuedJob
  param message : String

  def perform
    puts message
  end
end
```

### Step 2: Trigger that job

```crystal
# src/<somewher>/<somefile>.cr
PutsJob.new(message: "ohai background job").enqueue
```

### Step 3: Run your worker to process the job

```crystal
# src/worker.cr

Mosquito.configure do |settings|
  settings.redis_url = ENV["REDIS_URL"]
end

Mosquito::Runner.start
```

```text
crystal run src/worker.cr
```

### Success

```
> crystal run src/worker.cr
2017-11-06 17:07:29 - Mosquito is buzzing...
2017-11-06 17:07:51 - Running task puts_job<...> from puts_job
2017-11-06 17:07:51 - [PutsJob] ohai background job
2017-11-06 17:07:51 - task puts_job<...> succeeded, took 0.0 seconds
```

[More information about queued jobs](https://mosquito-cr.github.io/manual/index.html#queued-jobs) in the manual.

------

## Periodic Jobs

Periodic jobs run according to a predefined period -- once an hour, etc.

This periodic job:
```crystal
class PeriodicallyPutsJob < Mosquito::PeriodicJob
  run_every 1.minute

  def perform
    emotions = %w{happy sad angry optimistic political skeptical epuhoric}
    puts "The time is now #{Time.local} and the wizard is feeling #{emotions.sample}"
  end
end
```

Would produce this output:
```crystal
2017-11-06 17:20:13 - Mosquito is buzzing...
2017-11-06 17:20:13 - Queues: periodically_puts_job
2017-11-06 17:20:13 - Running task periodically_puts_job<...> from periodically_puts_job
2017-11-06 17:20:13 - [PeriodicallyPutsJob] The time is now 2017-11-06 17:20:13 and the wizard is feeling skeptical
2017-11-06 17:20:13 - task periodically_puts_job<...> succeeded, took 0.0 seconds
2017-11-06 17:21:14 - Queues: periodically_puts_job
2017-11-06 17:21:14 - Running task periodically_puts_job<...> from periodically_puts_job
2017-11-06 17:21:14 - [PeriodicallyPutsJob] The time is now 2017-11-06 17:21:14 and the wizard is feeling optimistic
2017-11-06 17:21:14 - task periodically_puts_job<...> succeeded, took 0.0 seconds
2017-11-06 17:22:15 - Queues: periodically_puts_job
2017-11-06 17:22:15 - Running task periodically_puts_job<...> from periodically_puts_job
2017-11-06 17:22:15 - [PeriodicallyPutsJob] The time is now 2017-11-06 17:22:15 and the wizard is feeling political
2017-11-06 17:22:15 - task periodically_puts_job<...> succeeded, took 0.0 seconds
```

[More information on periodic jobs](https://mosquito-cr.github.io/manual/index.html#periodic-jobs) in the manual.

## Advanced usage

For more advanced topics, including [use with Lucky Framework](https://mosquito-cr.github.io/manual/lucky_framework.html), [throttling or rate limiting](https://mosquito-cr.github.io/manual/rate_limiting.html), check out the [full manual](https://mosquito-cr.github.io/manual).

## Contributing

Contributions are welcome. Please fork the repository, commit changes on a branch, and then open a pull request.

### Crystal Versions

Mosquito aims to be compatible with the latest Crystal release, and the [latest patch for all post-1.0 minor crystal versions](https://github.com/mosquito-cr/mosquito/blob/master/.github/workflows/ci.yml#L17).

For development purposes [you're encouraged to stay in sync with `.tool-versions`](https://github.com/mosquito-cr/mosquito/blob/master/.tool-versions).

### Testing

`crystal spec` Will run the tests, or `make test` will too.


================================================
FILE: benchmark/benchmark.cr
================================================
require "../src/mosquito"
require "./jobs/*"

Mosquito.configure do |settings|
  settings.backend_connection_string = ENV["REDIS_URL"]? || "redis://localhost:6379/4"
  settings.publish_metrics = true
end

Mosquito.configuration.backend.flush

Log.setup do |c|
  backend = Log::IOBackend.new

  c.bind "redis.*", :error, backend
  c.bind "mosquito.*", :error, backend
end

stopping = false
Signal::INT.trap do
  if stopping
    puts "SIGINT received again, crash-exiting."
    exit 1
  end

  Mosquito::Runner.stop
  stopping = true
end

Mosquito::Runner.start spin: false

EventCount = 500
events = Deque(Time).new(EventCount)
event_count = 0
missed_messages = 0

channel = Mosquito.backend.subscribe(EmitMessageJob::PUBSUB_CHANNEL)

print "enqueuing benchmark jobs..."
10000.times {
  EmitMessageJob.new.enqueue
}
puts "done"

spawn do
  loop do
    break unless Mosquito::Runner.keep_running
    if missed_messages >= 100
      Mosquito::Runner.stop
      break
    end

    select
    when channel.receive
      events << Time.utc
      event_count += 1
    when timeout(100.milliseconds)
      missed_messages += 1
    end
  end
end

message = ->(span : Time::Span) do
  print "\r"
  print "Events: #{event_count} | "
  print "Span: #{span.total_seconds.round(2)} | "
  print "Rate: #{events.size.to_f./(span.to_f).round(2)} events/sec"
  print "    "
end

loop do
  break unless Mosquito::Runner.keep_running

  # if events.size >= EventCount
  #   (events.size - EventCount).times { events.shift }
  # end

  unless events.size >= 10
    print "\r"
    print "Waiting for events..."
    sleep 0.1.seconds
    next
  end

  message.call events.last - events.first
end

Mosquito::Runner.stop wait: true



puts
print "Total events: #{event_count} | "
print "Rate: #{events.size.to_f./(events.last.-(events.first).to_f).round(2)} events/sec"
puts


================================================
FILE: benchmark/jobs/emit_message_job.cr
================================================
class EmitMessageJob < Mosquito::QueuedJob
  PUBSUB_CHANNEL = "benchmark:messages"
  def perform
    number = Random::Secure.rand(100)
    Mosquito.backend.publish PUBSUB_CHANNEL, number.to_s
  end
end


================================================
FILE: demo/jobs/custom_serializers.cr
================================================
class CustomSerializersJob < Mosquito::QueuedJob
  param count : Int32

  def perform
    log "deserialized: #{count}"
    metadata.increment "run_count"
  end

  def deserialize_int32(raw : String) : Int32
    log "using custom serialization: #{raw}"

    raw.to_i32 * 10
  end
end

CustomSerializersJob.new(3).enqueue
CustomSerializersJob.new(12).enqueue
CustomSerializersJob.new(525_600).enqueue


================================================
FILE: demo/jobs/periodically_puts.cr
================================================
class PeriodicallyPuts < Mosquito::PeriodicJob
  run_every 3.seconds

  queue_name :demo_queue

  def perform
    log "Hello from PeriodicallyPuts"

    # For integration testing
    metadata.increment "run_count"
  end
end

# Periodic jobs do not need to be enqueued, they are executed automatically on schedule.


================================================
FILE: demo/jobs/queued_job.cr
================================================
class QueuedJob < Mosquito::QueuedJob
  param count : Int32

  queue_name :demo_queue

  def perform
    count.times do |i|
      log "ohai #{i}"
    end

    # For integration testing
    metadata.increment "run_count"
  end
end

QueuedJob.new(3).enqueue


================================================
FILE: demo/jobs/rate_limited_job.cr
================================================
class RateLimitedJob < Mosquito::QueuedJob
  before do
    log self.class.rate_limit_stats
  end

  include Mosquito::RateLimiter

  throttle limit: 3, per: 10.seconds

  param count : Int32

  def perform
    log @@rate_limit_key
  end
end

15.times do
  RateLimitedJob.new(3).enqueue
end


================================================
FILE: demo/jobs/unique_job.cr
================================================
class UniqueJob < Mosquito::QueuedJob
  include Mosquito::UniqueJob

  unique_for 1.hour, key: [:user_id]

  param user_id : Int64
  param message : String

  def perform
    log "Sending to user #{user_id}: #{message}"
    metadata.increment "run_count"
  end
end

# First enqueue — accepted
UniqueJob.new(user_id: 1_i64, message: "hello").enqueue

# Duplicate user_id — suppressed by uniqueness lock
UniqueJob.new(user_id: 1_i64, message: "hello again").enqueue

# Different user_id — accepted
UniqueJob.new(user_id: 2_i64, message: "hello").enqueue


================================================
FILE: demo/run.cr
================================================
require "../src/mosquito"

Mosquito.configure do |settings|
  settings.backend_connection_string = ENV["REDIS_URL"]? || "redis://localhost:6379/3"
  settings.idle_wait = 1.second
end

Mosquito.configuration.backend.flush

Log.setup do |c|
  backend = Log::IOBackend.new

  c.bind "*", :info, backend
  c.bind "redis.*", :warn, backend
  c.bind "mosquito.*", :info, backend
end

require "./jobs/*"

def expect_run_count(klass, expected)
  run_count = (klass.metadata["run_count"]? || "0").to_i
  if run_count != expected
    raise "Expected #{klass.name} to have run_count == #{expected}. But got #{run_count}"
  else
    puts "#{klass.name} was executed correctly."
  end
end

stopping = false
Signal::INT.trap do
  if stopping
    puts "SIGINT received again, crash-exiting."
    exit 1
  end

  Mosquito::Runner.stop
  stopping = true
end

Mosquito::Runner.start(spin: false)

count = 0
while count <= 19 && Mosquito::Runner.keep_running
  sleep 1.second
  count += 1
end

Mosquito::Runner.stop(wait: true)

puts "End of demo."
puts "----------------------------------"
puts "Checking integration test flags..."

expect_run_count(PeriodicallyPuts, 7)
expect_run_count(QueuedJob, 1)
expect_run_count(CustomSerializersJob, 3)
expect_run_count(RateLimitedJob, 3)
expect_run_count(UniqueJob, 2)


================================================
FILE: scripts/increment_version
================================================
#!/usr/bin/env crystal

require "yaml"
require "option_parser"

shard_yml = "shard.yml"

to_increment = "none"

OptionParser.parse! do |p|
  p.banner = "Usage: $0 -i <field>"
  p.on("-i field", "--increment=field", "Specifies the field to increment") do |name|
    destination = name
  end
  p.on("-h", "--help", "Show this help") { STDERR.puts p }
  p.invalid_option do |flag|
    STDERR.puts "ERROR: #{flag} is not a valid option."
    STDERR.puts p
    exit(1)
  end
end

document = File.read shard_yml
parsed = YAML.parse document
version = parsed["version"].as_s

major, minor, patch = version.split('.').map(&.to_i)

case to_increment
when "major"
  major += 1
  minor = 0
  patch = 0
when "minor"
  minor += 1
  patch = 0
when "patch"
  patch += 1
else
  STDERR.puts "No field to increment specified" if to_increment == "none"
end

parsed["version"] = "#{major}.#{minor}.#{patch}"
pp parsed.to_yaml


================================================
FILE: scripts/lib/increment_version.sh
================================================
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

print_help () {
  cat <<HELP
$0: increments a version number.

Usage: $0 -h x.y.z (major|minor|patch)

  x.y.z            The current version number. Eg. 56.02.17

  -h               Help.        Print this help and exit.

  major            Increment the major portion of the release, expressed as 'x' above.
  minor            Increment the minor portion of the release, expressed as 'y' above.
  patch            Increment the patch portion of the release, expressed as 'z' above.

  One of major, minor, or patch must be specified.

HELP

  exit 1
}

while getopts "h" opt; do
  case $opt in
    h) print_help ;;
  esac
done

shift $(($OPTIND - 1))
current_version=${1:-bad}
release_edition=${2:-bad}

case "$release_edition" in
  major|minor|patch) ;;
  *)
    echo "Error: could not increment by '$release_edition'."
    echo
    print_help
    ;;
esac

major=$( echo "$current_version" | awk -F. '{ print $1 }')
minor=$( echo "$current_version" | awk -F. '{ print $2 }')
patch=$( echo "$current_version" | awk -F. '{ print $3 }')

case "$release_edition" in
  major)
    major=$((major + 1))
    minor=0
    patch=0
    ;;

  minor)
    minor=$((minor + 1))
    patch=0
    ;;

  patch)
    patch=$((patch + 1))
    ;;

esac

new_version="$major.$minor.$patch"
echo "$new_version"


================================================
FILE: scripts/version_tag
================================================
#!/bin/bash

version=$(
  grep -e '^version' shard.yml \
    | awk '{print "v"$2}'
)

git tag $version


================================================
FILE: shard.yml
================================================
name: mosquito
version: 2.0.0

authors:
  - robacarp

crystal: '>= 1.19'

license: MIT

targets:
  demo:
    main: demo/run.cr

  mosquito:
    main: src/mosquito.cr

dependencies:
  redis:
    github: jgaskins/redis
    version: ~> 0.7

development_dependencies:
  minitest:
    github: ysbaddaden/minitest.cr
    version: ~> 1.6.0

  timecop:
    github: crystal-community/timecop.cr
    version: ~> 0.6.0


================================================
FILE: spec/helpers/bare_base_class.cr
================================================
module Mosquito
  class Base
    # Testing wedge which wipes out the JobRun mapping for the
    # duration of the block.
    def self.bare_mapping(&block)
      scheduled_job_runs = @@scheduled_job_runs
      @@scheduled_job_runs = [] of PeriodicJobRun

      mapping = @@mapping
      @@mapping = {} of String => Job.class

      yield

    ensure
      @@mapping = mapping unless mapping.nil?
      @@scheduled_job_runs = scheduled_job_runs unless scheduled_job_runs.nil?
    end
  end
end



================================================
FILE: spec/helpers/configuration_helper.cr
================================================
module Mosquito
  class_setter configuration

  macro temp_config(**settings)
    original_config = {{ @type }}.configuration.dup
    was_validated = {{ @type }}.configuration.validated

    {% for key, value in settings %}
      {{ @type }}.configuration.{{ key }} = {{ value }}
    {% end %}
    {{ @type }}.configuration.validated = false

    {{ yield }}

    {{ @type }}.configuration = original_config
    {{ @type }}.configuration.validated = was_validated
  end
end


================================================
FILE: spec/helpers/global_helpers.cr
================================================
module TestHelpers
  extend self

  # Testing wedge which provides a clean slate to ensure tests
  # aren't dependent on each other.
  def clean_slate(&block)
    Mosquito::Base.bare_mapping do
      backend = Mosquito.backend
      backend.flush

      Mosquito::TestBackend::Queue.flush_paused_queues!
      TestingLogBackend.instance.clear
      PubSub.instance.clear
      yield
    end
  end

  def backend : Mosquito::Backend
    Mosquito.configuration.backend
  end

  def testing_redis_url : String
    ENV["REDIS_URL"]? || "redis://localhost:6379/3"
  end
end

extend TestHelpers


================================================
FILE: spec/helpers/logging_helper.cr
================================================
require "log"

class TestingLogBackend < Log::MemoryBackend
  def self.instance
    @@instance ||= new
  end

  def clear
    @entries.clear
  end
end

class Minitest::Test
  def log_entries
    TestingLogBackend.instance.entries
  end

  def logs
    log_entries.map(&.message)
  end

  COLOR_STRIP = /\e\[\d+(;\d+)?m/

  private def logs_match(expected : Regex) : Bool
    log_entries
      .map(&.message)
      .map(&.gsub(COLOR_STRIP, ""))
      .any? { |entry| entry =~ expected }
  end

  private def logs_match(source : String, match_text : Regex) : Bool
    log_entries
      .select { |entry| entry.source == source }
      .map(&.message)
      .map(&.gsub(COLOR_STRIP, ""))
      .any? { |entry| entry =~ match_text }
  end

  def assert_logs_match(expected : String)
    assert_logs_match %r|#{expected}|
  end

  def assert_logs_match(expected : Regex)
    assert logs_match(expected), "Expected to logs to include #{expected}. Logs contained: \n#{log_entries.map(&.message).join("\n")}"
  end

  def refute_logs_match(expected : String)
    refute_logs_match %r|#{expected}|
  end

  def refute_logs_match(expected : Regex)
    refute logs_match(expected), "Expected to logs to not include #{expected}. Logs contained: \n#{log_entries.map(&.message).join("\n")}"
  end

  def assert_logs_match(source : String, expected : String)
    assert_logs_match source, %r|#{expected}|
  end

  def assert_logs_match(source : String, expected : Regex)
    assert logs_match(source, expected), "Expected to logs to include #{expected}. Logs contained: \n#{log_entries.map{|e| e.source + " " + e.message}.join("\n")}"
  end

  def refute_logs_match(source : String, expected : String)
    refute_logs_match source, %r|#{expected}|
  end

  def refute_logs_match(source : String, expected : Regex)
    refute logs_match(source, expected), "Expected to logs to not include #{expected}. Logs contained: \n#{log_entries.map{|e| e.source + " " + e.message}.join("\n")}"
  end

  def clear_logs
    TestingLogBackend.instance.clear
  end
end

Log.setup do |config|
  config.bind "*", :debug, TestingLogBackend.instance
  config.bind "redis.*", :warn, TestingLogBackend.instance
  config.bind "mosquito.*", :trace, TestingLogBackend.instance
end


================================================
FILE: spec/helpers/mock_coordinator.cr
================================================
class MockCoordinator < Mosquito::Runners::Coordinator
  getter schedule_count

  def initialize(queue_list : Mosquito::Runners::QueueList)
    super

    @schedule_count = 0
  end

  def only_if_coordinator : Nil
    if @always_coordinator
      yield
    else
      # yikes!
      # https://github.com/crystal-lang/crystal/issues/10399
      super do
        yield
      end
    end
  end

  def always_coordinator!(always = true)
    @always_coordinator = always
  end

  def schedule
    @schedule_count += 1
    super
  end
end


================================================
FILE: spec/helpers/mock_executor.cr
================================================
class MockExecutor < Mosquito::Runners::Executor
  setter work_unit : Mosquito::WorkUnit?

  def state=(state : Mosquito::Runnable::State)
    super
  end

  def run
    self.state = Mosquito::Runnable::State::Working
  end

  def stop(wait_group : WaitGroup = WaitGroup.new(1)) : WaitGroup
    self.state = Mosquito::Runnable::State::Stopping
    spawn do
      self.state = Mosquito::Runnable::State::Finished
      wait_group.done
    end
    wait_group
  end

  def receive_job
    job_pipeline.receive.job_run
  end
end


================================================
FILE: spec/helpers/mock_overseer.cr
================================================
class MockOverseer < Mosquito::Runners::Overseer
  property queue_list, coordinator, executors, work_handout, finished_notifier, dequeue_adapter

  def initialize
    @executor_count = Mosquito.configuration.executor_count
    @idle_wait = Mosquito.configuration.idle_wait
    @finished_notifier = Channel(Mosquito::WorkUnit?).new

    @queue_list = MockQueueList.new
    @coordinator = MockCoordinator.new queue_list
    @dequeue_adapter = Mosquito.configuration.dequeue_adapter
    @executors = [] of Mosquito::Runners::Executor
    @work_handout = Channel(Mosquito::WorkUnit).new
    @executors << build_executor
    observer.update_executor_list executors
  end

  def build_executor
    MockExecutor.new(self).as(Mosquito::Runners::Executor)
  end
end


================================================
FILE: spec/helpers/mock_queue_list.cr
================================================
class MockQueueList < Mosquito::Runners::QueueList
  setter state

  def discovered_queues : Array(Mosquito::Queue)
    @discovered_queues
  end

  def stop(wait_group : WaitGroup = WaitGroup.new(1)) : WaitGroup
    self.state = Mosquito::Runnable::State::Stopping
    spawn do
      self.state = Mosquito::Runnable::State::Finished
      wait_group.done
    end
    wait_group
  end
end


================================================
FILE: spec/helpers/mocks.cr
================================================
# A global place for global mocks

module PerformanceCounter
  def perform
    self.class.performed!
  end

  macro included
    class_getter performances = 0

    def self.performed!
      @@performances += 1
    end

    def self.reset_performance_counter!
      @@performances = 0
    end
  end
end

class JobWithPerformanceCounter < Mosquito::Job
  include PerformanceCounter
end

class PeriodicTestJob < Mosquito::PeriodicJob
  include PerformanceCounter
end

class QueuedTestJob < Mosquito::QueuedJob
  include PerformanceCounter
end

class QueueHookedTestJob < Mosquito::QueuedJob
  include PerformanceCounter

  property fail_before_hook = false
  property before_hook_ran = false
  property after_hook_ran = false
  property passed_job_config : Mosquito::JobRun? = nil

  before_enqueue do
    self.before_hook_ran = true
    self.passed_job_config = job

    if fail_before_hook
      false
    else
      true
    end
  end

  after_enqueue do
    self.after_hook_ran = true
    self.passed_job_config = job
  end
end


class PassingJob < QueuedTestJob
  def perform
    super
    true
  end
end

class FailingJob < QueuedTestJob
  property fail_with_exception = false
  property fail_with_retry = true
  property exception_message = "this is the reason #{name} failed"

  include PerformanceCounter

  def perform
    super

    case
    when fail_with_exception
      raise exception_message
    when ! fail_with_retry
      fail exception_message, retry: false
    else
      fail exception_message
    end
  end
end

class CustomRescheduleIntervalJob < PassingJob
  def reschedule_interval(retry_count)
    4.seconds
  end
end

class NonReschedulableFailingJob < FailingJob
  def rescheduleable?
    false
  end
end

class NotImplementedJob < Mosquito::Job
end

class JobWithConfig < PassingJob
  getter config = {} of String => String

  def vars_from(config : Hash(String, String))
    @config = config
  end
end

class JobWithNoParams < Mosquito::QueuedJob
  def perform
    log "no param job performed"
  end
end

class JobWithHooks < Mosquito::QueuedJob
  param should_fail : Bool

  before do
    log "Before Hook Executed"
  end

  after do
    log "After Hook Executed"
  end

  before do
    log "2nd Before Hook Executed"
    fail if should_fail
  end

  after do
    log "2nd After Hook Executed"
  end

  def perform
    log "Perform Executed"
  end
end

class EchoJob < Mosquito::QueuedJob
  queue_name "io_queue"

  param text : String

  def perform
    log text
  end
end

class MonthlyJob < Mosquito::PeriodicJob
  run_every 1.month

  def perform
    log "monthly job_run ran"
  end
end

class RateLimitedJob < Mosquito::QueuedJob
  include Mosquito::RateLimiter

  throttle key: "rate_limit", limit: Int32::MAX

  param should_fail : Bool = false
  param increment : Int32 = 1

  before do
    log "Before Hook Executed"
    fail if should_fail
  end

  def perform
    log "Performed"
  end

  def increment_run_count_by
    increment
  end
end

class PreemptingJob < Mosquito::QueuedJob
  include PerformanceCounter
  property preempt_until : Time? = nil

  before do
    preempt "test preemption", until: preempt_until
  end
end

class NonReschedulablePreemptingJob < Mosquito::QueuedJob
  include PerformanceCounter

  before do
    preempt "not reschedulable"
  end

  def rescheduleable? : Bool
    false
  end
end

class SleepyJob < Mosquito::QueuedJob
  class_property should_sleep = true

  def perform
    while self.class.should_sleep
      sleep 0.01.seconds
    end
  end
end

class SecondRateLimitedJob < Mosquito::QueuedJob
  include Mosquito::RateLimiter

  throttle key: "rate_limit", limit: Int32::MAX

  def perform
  end
end

class UniqueTestJob < Mosquito::QueuedJob
  include Mosquito::UniqueJob

  unique_for 1.hour

  param user_id : Int64
  param email_type : String

  def perform
    log "UniqueTestJob performed"
  end
end

class UniqueWithKeyJob < Mosquito::QueuedJob
  include Mosquito::UniqueJob

  unique_for 30.seconds, key: [:user_id]

  param user_id : Int64
  param message : String

  def perform
    log "UniqueWithKeyJob performed"
  end
end

class UniqueNoParamsJob < Mosquito::QueuedJob
  include Mosquito::UniqueJob

  unique_for 1.minute

  def perform
    log "UniqueNoParamsJob performed"
  end
end

Mosquito::Base.register_job_mapping "job_with_config", JobWithConfig
Mosquito::Base.register_job_mapping "job_with_performance_counter", JobWithPerformanceCounter
Mosquito::Base.register_job_mapping "failing_job", FailingJob
Mosquito::Base.register_job_mapping "non_reschedulable_failing_job", NonReschedulableFailingJob
Mosquito::Base.register_job_mapping "preempting_job", PreemptingJob
Mosquito::Base.register_job_mapping "non_reschedulable_preempting_job", NonReschedulablePreemptingJob

def job_run_config
  {
    "year" => "1752",
    "name" => "the year september lost 12 days",
  }
end

def create_job_run(type = "job_with_config", config = job_run_config)
  Mosquito::JobRun.new(type).tap do |job_run|
    job_run.config = config
    job_run.store
  end
end


================================================
FILE: spec/helpers/null_dequeue_adapter.cr
================================================
# A test adapter that always returns nil, simulating empty queues.
class NullDequeueAdapter < Mosquito::DequeueAdapter
  getter dequeue_count = 0

  def dequeue(queue_list : Mosquito::Runners::QueueList) : Mosquito::WorkUnit?
    @dequeue_count += 1
    nil
  end
end


================================================
FILE: spec/helpers/pub_sub.cr
================================================
module Mosquito::Observability::Publisher
  @[AlwaysInline]
  def publish(data : NamedTuple)
    metrics do
      Log.debug { "Publishing #{data} to #{@publish_context.originator}" }
      PubSub.instance.capture_message(@publish_context.originator, data.to_json)
    end
  end
end

class PubSub
  def self.instance
    @@instance ||= new
  end

  def self.eavesdrop : Array(Mosquito::Backend::BroadcastMessage)
    instance.listen
    yield
    instance.messages
  ensure
    instance.stop_listening
  end

  getter messages = [] of Mosquito::Backend::BroadcastMessage

  def initialize
    @listening = false
  end

  def listen
    @listening = true
  end

  def stop_listening
    @listening = false
  end

  def capture_message(originator : String, message : String)
    if @listening
      @messages << Mosquito::Backend::BroadcastMessage.new(originator, message)
    end
  end

  delegate clear, to: @messages

  module Helpers
    delegate eavesdrop, to: PubSub

    def assert_message_received(matcher : Regex) : Nil
      found = PubSub.instance.messages.find do |message|
        matcher === message.message
      end

      assert found, "Expected to find a message matching #{matcher.inspect}, but only found: #{PubSub.instance.messages.map(&.message).inspect}"
    end
  end
end


================================================
FILE: spec/helpers/spy_dequeue_adapter.cr
================================================
# A test adapter that tracks which queues were checked, in order.
class SpyDequeueAdapter < Mosquito::DequeueAdapter
  getter checked_queues = [] of String

  def dequeue(queue_list : Mosquito::Runners::QueueList) : Mosquito::WorkUnit?
    queue_list.queues.each do |q|
      @checked_queues << q.name
      if job_run = q.dequeue
        return Mosquito::WorkUnit.of(job_run, from: q)
      end
    end
  end
end


================================================
FILE: spec/mosquito/api/executor_config_spec.cr
================================================
require "../../spec_helper"

describe "Mosquito::Api::ExecutorConfig" do
  describe "global executor count" do
    it "returns nil when no override is stored" do
      clean_slate do
        result = Mosquito::Api::ExecutorConfig.stored_executor_count
        assert_nil result
      end
    end

    it "round-trips a global executor count" do
      clean_slate do
        Mosquito::Api::ExecutorConfig.store_executor_count(8)
        result = Mosquito::Api::ExecutorConfig.stored_executor_count
        assert_equal 8, result
      end
    end

    it "clears the global executor count" do
      clean_slate do
        Mosquito::Api::ExecutorConfig.store_executor_count(8)
        Mosquito::Api::ExecutorConfig.clear_executor_count

        result = Mosquito::Api::ExecutorConfig.stored_executor_count
        assert_nil result
      end
    end
  end

  describe "per-overseer executor count" do
    it "returns nil when no per-overseer override is stored" do
      clean_slate do
        result = Mosquito::Api::ExecutorConfig.stored_executor_count("gpu-worker-1")
        assert_nil result
      end
    end

    it "round-trips a per-overseer executor count" do
      clean_slate do
        Mosquito::Api::ExecutorConfig.store_executor_count(2, "gpu-worker-1")

        result = Mosquito::Api::ExecutorConfig.stored_executor_count("gpu-worker-1")
        assert_equal 2, result

        # Global is unaffected.
        global = Mosquito::Api::ExecutorConfig.stored_executor_count
        assert_nil global
      end
    end

    it "clears per-overseer without affecting global" do
      clean_slate do
        Mosquito::Api::ExecutorConfig.store_executor_count(8)
        Mosquito::Api::ExecutorConfig.store_executor_count(2, "gpu-worker-1")

        Mosquito::Api::ExecutorConfig.clear_executor_count("gpu-worker-1")

        per_overseer = Mosquito::Api::ExecutorConfig.stored_executor_count("gpu-worker-1")
        assert_nil per_overseer

        global = Mosquito::Api::ExecutorConfig.stored_executor_count
        assert_equal 8, global
      end
    end
  end

  describe ".resolve" do
    it "returns nil when nothing is stored" do
      clean_slate do
        result = Mosquito::Api::ExecutorConfig.resolve
        assert_nil result
      end
    end

    it "returns the global count when no overseer_id is given" do
      clean_slate do
        Mosquito::Api::ExecutorConfig.store_executor_count(8)
        result = Mosquito::Api::ExecutorConfig.resolve
        assert_equal 8, result
      end
    end

    it "prefers per-overseer over global" do
      clean_slate do
        Mosquito::Api::ExecutorConfig.store_executor_count(8)
        Mosquito::Api::ExecutorConfig.store_executor_count(2, "gpu-worker-1")

        result = Mosquito::Api::ExecutorConfig.resolve("gpu-worker-1")
        assert_equal 2, result
      end
    end

    it "falls back to global when per-overseer is not set" do
      clean_slate do
        Mosquito::Api::ExecutorConfig.store_executor_count(8)

        result = Mosquito::Api::ExecutorConfig.resolve("gpu-worker-1")
        assert_equal 8, result
      end
    end
  end

  describe "instance methods" do
    it "delegates to class-level helpers" do
      clean_slate do
        config = Mosquito::Api::ExecutorConfig.instance

        config.update(10)
        assert_equal 10, config.executor_count

        config.update(3, overseer_id: "worker-1")
        assert_equal 3, config.executor_count(overseer_id: "worker-1")

        config.clear(overseer_id: "worker-1")
        assert_nil config.executor_count(overseer_id: "worker-1")

        config.clear
        assert_nil config.executor_count
      end
    end
  end
end

describe "Mosquito::Api executor count convenience methods" do
  it "reads and writes global executor count" do
    clean_slate do
      Mosquito::Api.set_executor_count(12)
      assert_equal 12, Mosquito::Api.executor_count
    end
  end

  it "reads and writes per-overseer executor count" do
    clean_slate do
      Mosquito::Api.set_executor_count(4, overseer_id: "gpu-worker-1")
      assert_equal 4, Mosquito::Api.executor_count(overseer_id: "gpu-worker-1")

      # Global unaffected.
      assert_nil Mosquito::Api.executor_count
    end
  end
end


================================================
FILE: spec/mosquito/api/executor_spec.cr
================================================
require "../../spec_helper"

describe Mosquito::Api::Executor do
  let(executor_pipeline) { Channel(Mosquito::WorkUnit).new }
  let(finished_notifier) { Channel(Mosquito::WorkUnit?).new }
  let(job) { QueuedTestJob.new }
  let(job_run : Mosquito::JobRun) { job.enqueue }

  let(overseer) { MockOverseer.new }
  let(executor) { MockExecutor.new overseer.as(Mosquito::Runners::Overseer) }
  let(api) { Mosquito::Api::Executor.new executor.object_id.to_s }
  let(observer) { Mosquito::Observability::Executor.new executor }

  describe "publish context" do
    it "includes object_id" do
      assert_equal "executor:#{executor.object_id}", observer.publish_context.context
    end

    it "is nested under the overseer publish context" do
      assert_equal "mosquito:overseer:#{overseer.object_id}:executor:#{executor.object_id}", observer.publish_context.originator
    end
  end

  it "can read the current job and queue after being started, and clears it after" do
    Mosquito::Base.register_job_mapping job.class.name.underscore, job.class
    job_run.store
    job_run.build_job

    observer.execute job_run, job.class.queue do
      assert_equal job_run.id, api.current_job
      assert_equal job.class.queue.name, api.current_job_queue
    end

    assert api.current_job.nil?
    assert api.current_job_queue.nil?
  end

  it "returns a nil heartbeat before the executor has triggered it" do
    assert api.heartbeat.nil?
  end

  it "returns a valid heartbeat" do
    now = Time.utc
    Timecop.freeze now do
      observer.heartbeat!
    end

    # the heartbeat is stored as a unix epoch without millis
    assert_equal now.at_beginning_of_second, api.heartbeat
  end

  it "doesn't publish a heartbeat when metrics are disabled" do
    now = Time.utc

    Timecop.freeze now do
      executor.observer.heartbeat!
    end

    later = Time.utc + 1.minute
    Mosquito.temp_config(publish_metrics: false) do
      Timecop.freeze later do
        executor.observer.heartbeat!
      end
    end

    api = Mosquito::Api::Executor.new executor.object_id.to_s
    assert_equal now.at_beginning_of_second, api.heartbeat
  end

  it "publishes job started/finished events" do
    job_run.store
    job_run.build_job

    eavesdrop do
      observer.execute job_run, job.class.queue do
      end
    end

    assert_message_received /job-started/
    assert_message_received /job-finished/
  end

  it "measures and records average job duration" do
    job_run.store
    job_run.build_job

    # 100x the sleep duration below
    Timecop.scale(100) do
      observer.execute job_run, job.class.queue do
        sleep 0.01.seconds
      end
    end

    average_key = observer.average_key(job_run.type)
    average = Mosquito.backend.average(average_key)
    Mosquito.backend.delete average_key
    # assert that something > 0 comes back from the average.
    # backend tests cover calculating the average itself.
    assert average > 0
  end
end


================================================
FILE: spec/mosquito/api/job_run_spec.cr
================================================
require "../../spec_helper"

describe Mosquito::Api::JobRun do
  # the job run timestamps are stored as a unix epoch with millis, so nanosecond precision is lost.
  def at_beginning_of_millisecond(time)
    time - (time.nanosecond.nanoseconds) + (time.millisecond.milliseconds)
  end

  getter job : QueuedTestJob { QueuedTestJob.new }
  getter job_run : Mosquito::JobRun { job.build_job_run }
  getter api : Mosquito::Api::JobRun { Mosquito::Api::JobRun.new job_run.id }

  it "can look up a job run" do
    job_run.store
    assert api.found?
  end

  it "can look up a job run that doesn't exist" do
    api = Mosquito::Api::JobRun.new "not_a_real_id"
    refute api.found?
  end

  it "can retrieve the job parameters" do
    job_run = JobWithHooks.new(should_fail: false).build_job_run
    job_run.store
    api = Mosquito::Api::JobRun.new job_run.id
    assert_equal "false", api.runtime_parameters["should_fail"]
  end

  it "can retrieve the job type" do
    job_run.store
    assert_equal job.class.name.underscore, api.type
  end

  it "can retrieve the enqueue time" do
    now = Time.utc
    Timecop.freeze now do
      job_run.store
    end

    expected_time = at_beginning_of_millisecond now
    assert_equal expected_time, api.enqueue_time
  end

  it "can retrieve the retry count" do
    job_run.store
    assert_equal 0, api.retry_count
  end

  it "can retrieve the started at timestamp" do
    now = at_beginning_of_millisecond Time.utc
    job_run = create_job_run
    Timecop.freeze now do
      job_run.run
    end

    api = Mosquito::Api::JobRun.new(job_run.id)
    assert_equal now, api.started_at
  end

  it "can retrieve the finished_at timestamp" do
    now = at_beginning_of_millisecond Time.utc
    job_run = create_job_run
    Timecop.freeze now do
      job_run.run
    end

    api = Mosquito::Api::JobRun.new(job_run.id)
    assert_equal now, api.finished_at
  end
end


================================================
FILE: spec/mosquito/api/overseer_spec.cr
================================================
require "../../spec_helper"

describe Mosquito::Api::Overseer do
  let(:overseer) { MockOverseer.new }
  let(:api) { Mosquito::Api::Overseer.new(overseer.object_id.to_s) }
  let(:observer) { Observability::Overseer.new(overseer) }
  let(:executor) { MockExecutor.new(overseer.as(Mosquito::Runners::Overseer))}

  describe "publish context" do
    it "includes object_id" do
      assert_equal "overseer:#{overseer.object_id}", observer.publish_context.context
      assert_equal "mosquito:overseer:#{overseer.object_id}", observer.publish_context.originator
    end
  end

  it "allows fetching a list of executors" do
    assert_equal 1, api.executors.size
    observer.update_executor_list([executor, executor])
    assert_equal 2, api.executors.size
  end

  it "allows getting the latest heartbeat" do
    assert_nil api.last_heartbeat
    observer.heartbeat
    assert_instance_of Time, api.last_heartbeat
  end

  it "publishes the startup event" do
    eavesdrop do
      observer.starting
    end
    assert_message_received /started/
  end

  it "publishes the stopping event" do
    eavesdrop do
      observer.stopping
    end
    assert_message_received /stopped/
  end

  it "publishes the stopped event" do
    eavesdrop do
      observer.stopped
    end
    assert_message_received /exited/
  end

  it "publishes an event when an executor dies" do
    eavesdrop do
      observer.executor_died executor
    end
    assert_message_received /died/
  end

  it "publishes an event when an executor is created" do
    eavesdrop do
      observer.executor_created executor
    end
    assert_message_received /created/
  end
end


================================================
FILE: spec/mosquito/api/periodic_job_spec.cr
================================================
require "../../spec_helper"

describe Mosquito::Api::PeriodicJob do
  getter interval : Time::Span = 2.minutes

  describe "publish context" do
    it "includes the periodic job name" do
      clean_slate do
        Mosquito::Base.register_job_interval PeriodicTestJob, interval: interval
        job_run = Mosquito::Base.scheduled_job_runs.first
        observer = job_run.observer
        assert_equal "periodic_job:PeriodicTestJob", observer.publish_context.context
        assert_equal "mosquito:periodic_job:PeriodicTestJob", observer.publish_context.originator
      end
    end
  end

  it "can fetch a list of periodic jobs" do
    clean_slate do
      Mosquito::Base.register_job_interval PeriodicTestJob, interval: interval
      periodic_jobs = Mosquito::Api::PeriodicJob.all
      assert_equal 1, periodic_jobs.size
      assert_equal "PeriodicTestJob", periodic_jobs.first.name
      assert_equal interval, periodic_jobs.first.interval
    end
  end

  it "returns nil for last_executed_at when never run" do
    clean_slate do
      Mosquito::Base.register_job_interval PeriodicTestJob, interval: interval
      periodic_jobs = Mosquito::Api::PeriodicJob.all
      assert_nil periodic_jobs.first.last_executed_at
    end
  end

  it "returns the last executed time after a job runs" do
    now = Time.utc.at_beginning_of_second
    clean_slate do
      Mosquito::Base.register_job_interval PeriodicTestJob, interval: interval
      job_run = Mosquito::Base.scheduled_job_runs.first

      Timecop.freeze(now) do
        job_run.try_to_execute
      end

      periodic_jobs = Mosquito::Api::PeriodicJob.all
      assert_equal now, periodic_jobs.first.last_executed_at
    end
  end

  it "publishes an event when a periodic job is enqueued" do
    now = Time.utc.at_beginning_of_second
    clean_slate do
      Mosquito::Base.register_job_interval PeriodicTestJob, interval: interval

      eavesdrop do
        Timecop.freeze(now) do
          Mosquito::Base.scheduled_job_runs.first.try_to_execute
        end
      end

      assert_message_received /enqueued/
    end
  end
end


================================================
FILE: spec/mosquito/api/publisher_spec.cr
================================================
require "../../spec_helper"

describe Mosquito::Api::Publisher do
  let(executor_pipeline) { Channel(Mosquito::WorkUnit).new }
  let(finished_notifier) { Channel(Mosquito::WorkUnit?).new }
  let(job) { QueuedTestJob.new }
  let(job_run : Mosquito::JobRun) { job.enqueue }

  let(overseer) { MockOverseer.new }
  let(executor) { MockExecutor.new overseer.as(Mosquito::Runners::Overseer) }
  let(api) { Mosquito::Api::Executor.new executor.object_id.to_s }
  let(observer) { Mosquito::Observability::Executor.new executor }

  it "doesn't publish events when metrics are disabled" do
    job_run.store
    job_run.build_job

    PubSub.instance.clear
    published_messages = eavesdrop do
      Mosquito.temp_config(publish_metrics: false) do
        observer.execute job_run, job.class.queue do
        end
      end
    end

    assert_equal 0, published_messages.size
  end
end


================================================
FILE: spec/mosquito/api/queue_spec.cr
================================================
require "../../spec_helper"

describe Mosquito::Api::Queue do
  let(job_classes) {
    [QueuedTestJob, PassingJob, FailingJob, QueueHookedTestJob]
  }
  let(queued_test_job) { QueuedTestJob.new }
  let(passing_job) { PassingJob.new }
  let(queue : Mosquito::Queue) { queued_test_job.class.queue }
  let(observer : Mosquito::Observability::Queue) { queue.observer }

  describe "publish context" do
    it "includes the queue name" do
      assert_equal "queue:queued_test_job", observer.publish_context.context
      assert_equal "mosquito:queue:queued_test_job", observer.publish_context.originator
    end
  end

  it "can fetch a list of current queues" do
    clean_slate do
      queued_test_job.enqueue
      passing_job.enqueue
      expected_queues = ["queued_test_job", "passing_job"].sort
      queues = Mosquito::Api::Queue.all
      assert_equal 2, queues.size
      assert_equal expected_queues, queues.map(&.name).sort
    end
  end

  it "can fetch the size of a queue" do
    clean_slate do
      job_classes.map(&.new).each(&.enqueue)
      queues = Mosquito::Api::Queue.all
      queues.each do |queue|
        assert_equal 1, queue.size
      end
    end
  end

  it "can fetch the size details of a queue" do
    clean_slate do
      job_classes.map(&.new).each(&.enqueue)
      queues = Mosquito::Api::Queue.all
      sizes = queues.map(&.size_details)
      sizes.each do |size|
        assert_equal 1, size["waiting"]
        assert_equal 0, size["scheduled"]
        assert_equal 0, size["pending"]
        assert_equal 0, size["dead"]
      end
    end
  end

  it "can fetch job runs from a queue" do
    clean_slate do
      job_classes.each do |job_class|
        job = job_class.new
        job.enqueue
        api = Mosquito::Api::Queue.new job_class.queue.name
        job_runs = api.waiting_job_runs
        assert_equal 1, job_runs.size
        assert_equal job.class.name.underscore, job_runs.first.type
      end
    end
  end

  it "publishes an event when a job is enqueued" do
    eavesdrop do
      queued_test_job.enqueue
    end
    assert_message_received /enqueued/
  end

  it "publishes an event when a job is enqueued for later" do
    eavesdrop do
      queued_test_job.enqueue(60.seconds.from_now)
    end
    assert_message_received /enqueued/
  end

  it "publishes an event when a job is dequeued" do
    clean_slate do
      queued_test_job.enqueue

      eavesdrop do
        queue.dequeue
      end
    end

    assert_message_received /dequeued/
  end

  it "publishes an event when a job is rescheduled" do
    clean_slate do
      job_run = queued_test_job.build_job_run

      eavesdrop do
        queue.enqueue job_run
        queue.reschedule job_run, 60.seconds.from_now
      end
    end

    assert_message_received /rescheduled/
  end

  it "publishes an event when a job is forgotten" do
    clean_slate do
      job_run = queued_test_job.build_job_run

      eavesdrop do
        queue.forget job_run
      end
    end

    assert_message_received /forgotten/
  end

  it "publishes an event when a job is banished" do
    clean_slate do
      job_run = queued_test_job.build_job_run

      eavesdrop do
        queue.banish job_run
      end
    end

    assert_message_received /banished/
  end
end


================================================
FILE: spec/mosquito/api_spec.cr
================================================
require "../spec_helper"

describe Mosquito::Api do
  let(queued_test_job) { QueuedTestJob.new }
  let(passing_job) { PassingJob.new }

  it "can fetch a list of queues" do
    clean_slate do
      queued_test_job.enqueue
      passing_job.enqueue
      queues = Mosquito::Api.list_queues
      assert_equal 2, queues.size
      queue_names = queues.map(&.name)
      assert_includes queue_names, queued_test_job.class.queue.name
      assert_includes queue_names, passing_job.class.queue.name
    end
  end
end


================================================
FILE: spec/mosquito/backend/deleting_spec.cr
================================================

require "../../spec_helper"

describe "Backend deleting" do
  getter queue_name : String { "test#{rand(1000)}" }
  getter queue : Mosquito::Backend::Queue { backend.queue queue_name }

  getter sample_data do
    { "test" => "#{rand(1000)}" }
  end

  getter key : String { "key-#{rand 1000}" }
  getter field : String { "field-#{rand 1000}" }

  getter job_run : Mosquito::JobRun { Mosquito::JobRun.new("mock_job_run") }

  describe "delete" do
    it "deletes immediately" do
      backend.store key, sample_data
      backend.delete key
      blank_data = {} of String => String
      assert_equal blank_data, backend.retrieve(key)
    end

    it "deletes at a ttl" do
      # Since redis is outside the control of timecop, this test is just showing
      # that #delete can be called with a ttl and we trust redis to do it's job.
      backend.store key, sample_data
      backend.delete key, in: 1.second
    end
  end

  describe "self.flush" do
    it "wipes the database" do
      clean_slate do
        backend.set key, field, "1"
        backend.flush
        assert_nil backend.get key, field
      end
    end
  end

  describe "#flush" do
    it "empties the queues" do
      clean_slate do
        # add a job_run to waiting
        queue.enqueue job_run

        # add a job_run to scheduled
        queue.schedule job_run, at: 1.second.from_now

        # move a job_run to pending
        pending_job_run = queue.dequeue

        # add a job_run to the dead queue
        queue.terminate job_run

        queue.flush
        empty_set = [] of String

        assert_equal empty_set, queue.list_waiting
        assert_equal empty_set, queue.list_scheduled
        assert_equal empty_set, queue.list_pending
        assert_equal empty_set, queue.list_dead
      end
    end

    it "but doesn't truncate the database" do
      clean_slate do
        backend.set key, field, "value"
        queue.flush
        assert_equal "value", backend.get key, field
      end
    end
  end
end


================================================
FILE: spec/mosquito/backend/executor_spec.cr
================================================
require "../../spec_helper"

describe Mosquito::Backend do
  getter key : String { "key-#{rand 1000}" }

  it "can calculate an average" do
    backend.average_push key, 10
    backend.average_push key, 20
    backend.average_push key, 30

    assert_equal 20, backend.average key
  end

  it "correctly rolls off old values for the window size" do
    backend.average_push key, 10, window_size: 3
    backend.average_push key, 20, window_size: 3
    backend.average_push key, 30, window_size: 3
    backend.average_push key, 40, window_size: 3
    backend.average_push key, 50, window_size: 3

    assert_equal 40, backend.average key
  end
end


================================================
FILE: spec/mosquito/backend/expiring_list_spec.cr
================================================
require "../../spec_helper"

describe Mosquito::RedisBackend do
  describe "expiring lists" do
    it "can add an item to a list" do
      now = Time.utc
      key = "exp-list-test"
      items = ["item1", "item2", "item3"]

      redis_backend = backend.as(Mosquito::RedisBackend)

      Timecop.freeze now do
        redis_backend.expiring_list_push key, items[0]
      end

      Timecop.freeze now + 1.second do
        redis_backend.expiring_list_push key, items[1]
      end

      Timecop.freeze now + 2.seconds do
        redis_backend.expiring_list_push key, items[2]
      end

      found_items = redis_backend.expiring_list_fetch(key, now + 1.second)
      assert_equal [items[2]], found_items
    end
  end
end


================================================
FILE: spec/mosquito/backend/hash_storage_spec.cr
================================================
require "../../spec_helper"

describe "Backend hash storage" do
  getter sample_data : Hash(String,String) { { "test" => "#{rand(1000)}" } }
  getter key : String { "key-#{rand 1000}" }
  getter field : String { "field-#{rand 1000}" }

  it "can store and retrieve" do
    backend.store key, sample_data
    retrieved_data = backend.retrieve key
    assert_equal sample_data, retrieved_data
  end

  describe "self.get and set" do
    it "sets and retrieves a value from a hash" do
      backend.set(key, field, "truth")
      assert_equal "truth", backend.get(key, field)
    end
  end

  describe "self.increment" do
    it "adds one" do
      backend.set(key, field, "1")
      assert_equal 2, backend.increment(key, field)
    end

    it "can add arbitrary values" do
      backend.set(key, field, "1")
      assert_equal 4, backend.increment(key, field, by: 3)
    end
  end
end


================================================
FILE: spec/mosquito/backend/inspection_spec.cr
================================================
require "../../spec_helper"

describe "Backend inspection" do
  getter backend_name : String { "test#{rand(1000)}" }
  getter queue : Mosquito::Backend::Queue { backend.queue backend_name }

  getter job : QueuedTestJob { QueuedTestJob.new }
  getter job_run : Mosquito::JobRun { Mosquito::JobRun.new("mock_job_run") }

  describe "size" do
    def fill_queues
      # add to waiting queue
      queue.enqueue job_run
      queue.enqueue job_run

      # move 1 from waiting to pending queue
      pending_t = queue.dequeue

      # add to scheduled queue
      queue.schedule job_run, at: 1.second.from_now

      # add to dead queue
      queue.terminate job_run
    end

    it "returns the size of the named q" do
      clean_slate do
        fill_queues
        assert_equal 4, queue.size
      end
    end

    it "returns the size of the named q (without the dead_q)" do
      clean_slate do
        fill_queues
        assert_equal 3, queue.size(include_dead: false)
      end
    end
  end

  describe "list" do
    it "can list the waiting jobs" do
      clean_slate do
        expected_job_runs = Array(Mosquito::JobRun).new(3) { Mosquito::JobRun.new("mock_job_run") }
        expected_job_runs.each { |job_run| queue.enqueue job_run }
        expected_job_run_ids = expected_job_runs.map { |job_run| job_run.id }.sort

        actual_job_runs = queue.list_waiting.sort
        assert_equal 3, actual_job_runs.size

        assert_equal expected_job_run_ids, actual_job_runs
      end
    end

    it "can list the scheduled jobs" do
      clean_slate do
        expected_job_runs = Array(Mosquito::JobRun).new(3) { Mosquito::JobRun.new("mock_job_run") }
        expected_job_runs.each { |job_run| queue.schedule job_run, at: 1.second.from_now }
        expected_job_run_ids = expected_job_runs.map { |job_run| job_run.id }.sort

        actual_job_runs = queue.list_scheduled.sort
        assert_equal 3, actual_job_runs.size

        assert_equal expected_job_run_ids, actual_job_runs
      end
    end

    it "can list the pending jobs" do
      clean_slate do
        expected_job_runs = Array(Mosquito::JobRun).new(3) { Mosquito::JobRun.new("mock_job_run").tap(&.store) }

        expected_job_runs.each { |job_run| queue.enqueue job_run }
        expected_job_run_ids = 3.times.map { queue.dequeue.not_nil!.id }.to_a.sort

        actual_job_runs = queue.list_pending.sort
        assert_equal 3, actual_job_runs.size

        assert_equal expected_job_run_ids, actual_job_runs
      end
    end

    it "can list the dead jobs" do
      clean_slate do
        expected_job_runs = Array(Mosquito::JobRun).new(3) { Mosquito::JobRun.new("mock_job_run") }
        expected_job_runs.each { |job_run| queue.terminate job_run }
        expected_job_run_ids = expected_job_runs.map { |job_run| job_run.id }.sort

        actual_job_runs = queue.list_dead.sort
        assert_equal 3, actual_job_runs.size

        assert_equal expected_job_run_ids, actual_job_runs
      end
    end
  end
end


================================================
FILE: spec/mosquito/backend/lock_spec.cr
================================================
require "../../spec_helper"

describe "distributed locking" do
  getter key : String { "testing:backend:lock" }
  getter instance_id : String { "abcd" }
  getter ttl : Time::Span { 1.second }

  def ensure_unlock(&block)
    yield
    Mosquito.backend.delete key
  end

  it "locks" do
    ensure_unlock do
      got_it = Mosquito.backend.lock? key, instance_id, ttl
      assert got_it
    end
  end

  it "doesn't double lock" do
    ensure_unlock do
      hold = Mosquito.backend.lock? key, "abcd", ttl
      assert hold

      try = Mosquito.backend.lock? key, "wxyz", ttl
      refute try
    end
  end

  it "locks after unlock" do
    ensure_unlock do
      hold = Mosquito.backend.lock? key, "abcd", ttl
      assert hold

      Mosquito.backend.unlock key, instance_id

      try = Mosquito.backend.lock? key, "wxyz", ttl
      assert try
    end
  end

  it "renews a lock held by the same instance" do
    ensure_unlock do
      hold = Mosquito.backend.lock? key, instance_id, ttl
      assert hold

      renewed = Mosquito.backend.renew_lock? key, instance_id, ttl
      assert renewed
    end
  end

  it "doesn't renew a lock held by another instance" do
    ensure_unlock do
      hold = Mosquito.backend.lock? key, "abcd", ttl
      assert hold

      renewed = Mosquito.backend.renew_lock? key, "wxyz", ttl
      refute renewed
    end
  end

  it "doesn't renew a lock that doesn't exist" do
    ensure_unlock do
      renewed = Mosquito.backend.renew_lock? key, instance_id, ttl
      refute renewed
    end
  end
end


================================================
FILE: spec/mosquito/backend/overseer_spec.cr
================================================
require "../../spec_helper"

describe Mosquito::Backend do
  it "can keep a list of overseers" do
    clean_slate do
      overseer_ids = ["overseer1", "overseer2", "overseer3"]
      overseer_ids.each do |overseer_id|
        Mosquito.backend.register_overseer overseer_id
      end

      assert_equal overseer_ids, Mosquito.backend.list_overseers
    end
  end

  it "can deregister an overseer" do
    clean_slate do
      overseer_ids = ["overseer1", "overseer2", "overseer3"]
      overseer_ids.each do |overseer_id|
        Mosquito.backend.register_overseer overseer_id
      end

      Mosquito.backend.deregister_overseer "overseer2"

      assert_equal ["overseer1", "overseer3"], Mosquito.backend.list_overseers
    end
  end
end


================================================
FILE: spec/mosquito/backend/queueing_spec.cr
================================================
require "../../spec_helper"

describe "Backend Queues" do
  getter backend_name : String { "test#{rand(1000)}" }
  getter queue : Mosquito::Backend::Queue { backend.queue backend_name }

  getter job : QueuedTestJob { QueuedTestJob.new }
  getter job_run : Mosquito::JobRun { Mosquito::JobRun.new("mock_job_run") }

  describe "list_queues" do
    def fill_queues
      names = %w|test1 test2 test3 test4|

      names[0..3].each do |queue_name|
        backend.queue(queue_name).enqueue job_run
      end

      backend.queue(names.last).schedule job_run, at: 1.second.from_now
    end

    def fill_uncounted_queues
      names = %w|test5 test6 test7 test8|

      names[0..3].each do |queue_name|
        backend.queue(queue_name).tap do |q|
          q.enqueue job_run
          q.dequeue
        end
      end

      backend.queue(names.last).terminate job_run
    end

    it "can get a list of available queues" do
      clean_slate do
        fill_queues
        assert_equal %w|test1 test2 test3 test4|, backend.list_queues.sort
      end
    end

    it "de-dups the queue list" do
      clean_slate do
        fill_queues
        assert_equal %w|test1 test2 test3 test4|, backend.list_queues.sort
      end
    end
  end

  describe "schedule" do
    it "adds a job_run to the schedule_q at the time" do
      clean_slate do
        timestamp = 2.seconds.from_now
        job_run = job.build_job_run
        queue.schedule job_run, at: timestamp
        assert_equal Time.unix_ms(timestamp.to_unix_ms), queue.scheduled_job_run_time job_run
      end
    end
  end

  describe "deschedule" do
    it "returns a job_run if it's due" do
      clean_slate do
        run_time = Time.utc - 2.seconds
        job_run = job.build_job_run
        job_run.store
        queue.schedule job_run, at: run_time

        overdue_job_runs = queue.deschedule
        assert_equal [job_run], overdue_job_runs
      end
    end

    it "returns a blank array when no job_runs exist" do
      clean_slate do
        overdue_job_runs = queue.deschedule
        assert_empty overdue_job_runs
      end
    end

    it "doesn't return job_runs which aren't yet due" do
      clean_slate do
        run_time = Time.utc + 2.seconds
        job_run = job.build_job_run
        job_run.store
        queue.schedule job_run, at: run_time

        overdue_job_runs = queue.deschedule
        assert_empty overdue_job_runs
      end
    end
  end

  describe "enqueue" do
    it "puts a job_run on the waiting_q" do
      clean_slate do
        job_run = job.build_job_run
        queue.enqueue job_run
        waiting_job_runs = queue.list_waiting
        assert_equal [job_run.id], waiting_job_runs
      end
    end
  end

  describe "dequeue" do
    it "returns a job_run object when one is waiting" do
      clean_slate do
        job_run = job.build_job_run
        job_run.store
        queue.enqueue job_run
        waiting_job_run = queue.dequeue
        assert_equal job_run, waiting_job_run
      end
    end

    it "moves the job_run from waiting to pending" do
      clean_slate do
        job_run = job.build_job_run
        job_run.store
        queue.enqueue job_run
        waiting_job_run = queue.dequeue
        pending_job_runs = queue.list_pending
        assert_equal [job_run.id], pending_job_runs
      end
    end

    it "returns nil when nothing is waiting" do
      clean_slate do
        assert_equal nil, queue.dequeue
      end
    end

    it "returns nil when a job_run is queued but not stored" do
      clean_slate do
        job_run = job.build_job_run
        # job_run.store # explicitly don't store this one
        queue.enqueue job_run
        waiting_job_run = queue.dequeue
        assert_nil waiting_job_run
      end
    end
  end

  describe "finish" do
    it "removes the job_run from the pending queue" do
      clean_slate do
        job_run = job.build_job_run
        job_run.store

        # first move the job_run from waiting to pending
        queue.enqueue job_run
        waiting_job_run = queue.dequeue
        assert_equal job_run, waiting_job_run

        # now finish it
        queue.finish job_run

        pending_job_runs = queue.list_pending
        assert_empty pending_job_runs
      end
    end
  end

  describe "terminate" do
    it "adds a job_run to the dead queue" do
      clean_slate do
        job_run = job.build_job_run
        job_run.store

        # first move the job_run from waiting to pending
        queue.enqueue job_run
        waiting_job_run = queue.dequeue
        assert_equal job_run, waiting_job_run

        # now terminate it
        queue.terminate job_run

        dead_job_runs = queue.list_dead
        assert_equal [job_run.id], dead_job_runs
      end
    end
  end

end


================================================
FILE: spec/mosquito/backend_spec.cr
================================================
require "../spec_helper"

# These tests are explicitly for code which is inherited from the abstract Backend
describe Mosquito::Backend do
  it "can build a key with two strings" do
    assert_equal "mosquito:one:two", Mosquito.backend.build_key("one", "two")
  end

  it "can build a key with an array" do
    assert_equal "mosquito:one:two", Mosquito.backend.build_key(["one", "two"])
  end

  it "can build a key with a tuple" do
    assert_equal "mosquito:one:two", Mosquito.backend.build_key(*{"one", "two"})
  end

  it "can be initialized with a string name" do
    Mosquito.backend.queue "string_backend"
  end

  it "can be initialized with a symbol name" do
    Mosquito.backend.queue :symbol_backend
  end

  it "can update a key with a hash" do
    Mosquito.backend.set "key", {"field" => "value", "field2" => "value2"}
    assert_equal "value", Mosquito.backend.get("key", "field")
    assert_equal "value2", Mosquito.backend.get("key", "field2")
  end
end


================================================
FILE: spec/mosquito/base_spec.cr
================================================
require "../spec_helper"

describe Mosquito::Base do
  it "keeps a list of scheduled job_runs" do
    Base.bare_mapping do
      Base.register_job_interval PeriodicTestJob, 1.minute
      assert_equal PeriodicTestJob, Base.scheduled_job_runs.first.class
    end
  end

  it "correctly maps job classes from type strings" do
    Base.bare_mapping do
      Base.register_job_mapping "fizzbuzz", QueuedTestJob
      assert_equal QueuedTestJob, Base.job_for_type "fizzbuzz"
    end
  end
end


================================================
FILE: spec/mosquito/configuration_spec.cr
================================================
require "../spec_helper"

describe "Mosquito Config" do
  it "allows setting / retrieving the connection string" do
    Mosquito.temp_config do
      Mosquito.configuration.backend_connection_string = testing_redis_url
      assert_equal testing_redis_url, Mosquito.configuration.backend_connection_string
    end
  end

  it "enforces missing settings are set" do
    config = Mosquito::Configuration.new
    assert_raises do
      config.validate
    end
  end

  it "allows setting idle_wait as a float" do
    test_value = 2.4
    Mosquito.temp_config do
      Mosquito.configuration.idle_wait = test_value
      assert_equal test_value.seconds, Mosquito.configuration.idle_wait
    end
  end

  it "allows setting idle_wait as a time span" do
    test_value = 2.seconds

    Mosquito.temp_config do
      Mosquito.configuration.idle_wait = test_value
      assert_equal test_value, Mosquito.configuration.idle_wait
    end
  end

  it "allows setting successful_job_ttl" do
    test_value = 2

    Mosquito.temp_config do
      Mosquito.configuration.successful_job_ttl = test_value
      assert_equal test_value, Mosquito.configuration.successful_job_ttl
    end
  end

  it "allows setting failed_job_ttl" do
    test_value = 2

    Mosquito.temp_config do
      Mosquito.configuration.failed_job_ttl = test_value
      assert_equal test_value, Mosquito.configuration.failed_job_ttl
    end
  end

  it "allows setting global_prefix string" do
    test_value = "yolo"

    Mosquito.temp_config do
      Mosquito.configuration.global_prefix = test_value
      assert_equal test_value, Mosquito.configuration.global_prefix
      Mosquito.configuration.backend.build_key("test").must_equal "yolo:mosquito:test"
    end
  end

  it "allows setting global_prefix nillable" do
    test_value = nil

    Mosquito.temp_config do
      Mosquito.configuration.global_prefix = test_value
      assert_equal test_value, Mosquito.configuration.global_prefix
      Mosquito.configuration.backend.build_key("test").must_equal "mosquito:test"
    end
  end

  it "validates when backend_connection_string is set" do
    Mosquito.temp_config do
      Mosquito.configuration.backend_connection_string = testing_redis_url
      Mosquito.configuration.validate
    end
  end
end


================================================
FILE: spec/mosquito/dequeue_adapters/concurrency_limited_dequeue_adapter_spec.cr
================================================
require "../../spec_helper"

describe "Mosquito::ConcurrencyLimitedDequeueAdapter" do
  getter(overseer : MockOverseer) { MockOverseer.new }
  getter(queue_list : MockQueueList) { overseer.queue_list.as(MockQueueList) }

  def register(job_class : Mosquito::Job.class)
    Mosquito::Base.register_job_mapping job_class.name.underscore, job_class
    queue_list.queues << job_class.queue
  end

  it "dequeues a job when under the limit" do
    clean_slate do
      register QueuedTestJob
      expected_job_run = QueuedTestJob.new.enqueue

      adapter = Mosquito::ConcurrencyLimitedDequeueAdapter.new({
        "queued_test_job" => 3,
      })

      result = adapter.dequeue(queue_list)
      refute_nil result
      if result
        assert_equal expected_job_run, result.job_run
        assert_equal QueuedTestJob.queue, result.queue
      end
    end
  end

  it "returns nil when no jobs are available" do
    clean_slate do
      register QueuedTestJob

      adapter = Mosquito::ConcurrencyLimitedDequeueAdapter.new({
        "queued_test_job" => 3,
      })

      result = adapter.dequeue(queue_list)
      assert_nil result
    end
  end

  it "skips a queue that has reached its concurrency limit" do
    clean_slate do
      register QueuedTestJob
      3.times { QueuedTestJob.new.enqueue }

      adapter = Mosquito::ConcurrencyLimitedDequeueAdapter.new({
        "queued_test_job" => 2,
      })

      # Dequeue twice — should succeed and fill the limit.
      result1 = adapter.dequeue(queue_list)
      refute_nil result1
      assert_equal 1, adapter.active_count("queued_test_job")

      result2 = adapter.dequeue(queue_list)
      refute_nil result2
      assert_equal 2, adapter.active_count("queued_test_job")

      # Third dequeue should be blocked by the limit.
      result3 = adapter.dequeue(queue_list)
      assert_nil result3
    end
  end

  it "allows dequeue again after finished_with" do
    clean_slate do
      register QueuedTestJob
      3.times { QueuedTestJob.new.enqueue }

      adapter = Mosquito::ConcurrencyLimitedDequeueAdapter.new({
        "queued_test_job" => 1,
      })

      # Fill the single slot.
      result1 = adapter.dequeue(queue_list)
      refute_nil result1
      assert_equal 1, adapter.active_count("queued_test_job")

      # Blocked.
      result2 = adapter.dequeue(queue_list)
      assert_nil result2

      # Signal that the job finished.
      adapter.finished_with(result1.not_nil!.job_run, result1.not_nil!.queue)
      assert_equal 0, adapter.active_count("queued_test_job")

      # Now dequeue should work again.
      result3 = adapter.dequeue(queue_list)
      refute_nil result3
    end
  end

  it "does not limit queues not in the limits table" do
    clean_slate do
      register QueuedTestJob
      5.times { QueuedTestJob.new.enqueue }

      # No limit configured for queued_test_job.
      adapter = Mosquito::ConcurrencyLimitedDequeueAdapter.new({
        "other_queue" => 1,
      })

      # Should dequeue all 5 without blocking.
      5.times do |i|
        result = adapter.dequeue(queue_list)
        refute_nil result, "Expected dequeue ##{i + 1} to succeed"
      end
    end
  end

  it "enforces independent limits across multiple queues" do
    clean_slate do
      register QueuedTestJob
      register EchoJob
      3.times { QueuedTestJob.new.enqueue }
      3.times { EchoJob.new(text: "hello").enqueue }

      adapter = Mosquito::ConcurrencyLimitedDequeueAdapter.new({
        "queued_test_job" => 1,
        "io_queue"        => 2,
      })

      # Saturate queued_test_job (limit 1).
      # Because of shuffle we may get either queue first, so keep
      # dequeuing until the counters match the limits.
      results = [] of Mosquito::WorkUnit
      6.times do
        if r = adapter.dequeue(queue_list)
          results << r
        end
      end

      assert_equal 1, adapter.active_count("queued_test_job")
      assert_equal 2, adapter.active_count("io_queue")
      assert_equal 3, results.size
    end
  end

  it "finished_with does not go below zero" do
    adapter = Mosquito::ConcurrencyLimitedDequeueAdapter.new({
      "queued_test_job" => 3,
    })

    job_run = Mosquito::JobRun.new("queued_test_job")
    queue = Mosquito::Queue.new("queued_test_job")
    adapter.finished_with(job_run, queue)
    assert_equal 0, adapter.active_count("queued_test_job")
  end

  it "can be used via the overseer" do
    clean_slate do
      adapter = Mosquito::ConcurrencyLimitedDequeueAdapter.new({
        "queued_test_job" => 5,
      })
      overseer.dequeue_adapter = adapter

      register QueuedTestJob
      expected_job_run = QueuedTestJob.new.enqueue

      result = overseer.dequeue_job?
      refute_nil result
      if result
        assert_equal expected_job_run, result.job_run
      end
    end
  end
end


================================================
FILE: spec/mosquito/dequeue_adapters/remote_config_dequeue_adapter_spec.cr
================================================
require "../../spec_helper"

describe "Mosquito::RemoteConfigDequeueAdapter" do
  getter(overseer : MockOverseer) { MockOverseer.new }
  getter(queue_list : MockQueueList) { overseer.queue_list.as(MockQueueList) }

  def register(job_class : Mosquito::Job.class)
    Mosquito::Base.register_job_mapping job_class.name.underscore, job_class
    queue_list.queues << job_class.queue
  end

  it "uses defaults when no remote config is present" do
    clean_slate do
      register QueuedTestJob
      3.times { QueuedTestJob.new.enqueue }

      adapter = Mosquito::RemoteConfigDequeueAdapter.new(
        defaults: {"queued_test_job" => 2},
        refresh_interval: 0.seconds,
      )

      # Two dequeues should succeed.
      result1 = adapter.dequeue(queue_list)
      refute_nil result1

      result2 = adapter.dequeue(queue_list)
      refute_nil result2

      # Third should be blocked by the default limit of 2.
      result3 = adapter.dequeue(queue_list)
      assert_nil result3
    end
  end

  it "picks up remote limits from the backend" do
    clean_slate do
      register QueuedTestJob
      3.times { QueuedTestJob.new.enqueue }

      # Default allows 2, but remote overrides to 1.
      adapter = Mosquito::RemoteConfigDequeueAdapter.new(
        defaults: {"queued_test_job" => 2},
        refresh_interval: 0.seconds,
      )

      Mosquito::RemoteConfigDequeueAdapter.store_limits({"queued_test_job" => 1})

      result1 = adapter.dequeue(queue_list)
      refute_nil result1

      # Should be blocked — remote limit is 1.
      result2 = adapter.dequeue(queue_list)
      assert_nil result2
    end
  end

  it "merges remote limits on top of defaults" do
    clean_slate do
      adapter = Mosquito::RemoteConfigDequeueAdapter.new(
        defaults: {"queue_a" => 3, "queue_b" => 5},
        refresh_interval: 0.seconds,
      )

      # Remote only overrides queue_a and adds queue_c.
      Mosquito::RemoteConfigDequeueAdapter.store_limits({
        "queue_a" => 1,
        "queue_c" => 7,
      })

      adapter.refresh_limits

      assert_equal 1, adapter.limits["queue_a"]
      assert_equal 5, adapter.limits["queue_b"]
      assert_equal 7, adapter.limits["queue_c"]
    end
  end

  it "falls back to defaults when remote config is cleared" do
    clean_slate do
      adapter = Mosquito::RemoteConfigDequeueAdapter.new(
        defaults: {"queue_a" => 3},
        refresh_interval: 0.seconds,
      )

      Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 1})
      adapter.refresh_limits
      assert_equal 1, adapter.limits["queue_a"]

      Mosquito::RemoteConfigDequeueAdapter.clear_limits
      adapter.refresh_limits
      assert_equal 3, adapter.limits["queue_a"]
    end
  end

  it "respects refresh_interval and does not poll on every dequeue" do
    clean_slate do
      register QueuedTestJob
      3.times { QueuedTestJob.new.enqueue }

      adapter = Mosquito::RemoteConfigDequeueAdapter.new(
        defaults: {"queued_test_job" => 3},
        refresh_interval: 1.hour,
      )

      # First dequeue triggers the initial refresh.
      adapter.dequeue(queue_list)

      # Store a tighter limit — but it should NOT take effect
      # because the refresh interval hasn't elapsed.
      Mosquito::RemoteConfigDequeueAdapter.store_limits({"queued_test_job" => 1})

      result2 = adapter.dequeue(queue_list)
      refute_nil result2, "Expected dequeue to succeed because refresh hasn't fired"
    end
  end

  it "preserves in-flight counts when limits are refreshed" do
    clean_slate do
      register QueuedTestJob
      2.times { QueuedTestJob.new.enqueue }

      adapter = Mosquito::RemoteConfigDequeueAdapter.new(
        defaults: {"queued_test_job" => 1},
        refresh_interval: 0.seconds,
      )

      result1 = adapter.dequeue(queue_list)
      refute_nil result1
      assert_equal 1, adapter.active_count("queued_test_job")

      # Refresh with new limits — must not reset the in-flight counter.
      Mosquito::RemoteConfigDequeueAdapter.store_limits({"queued_test_job" => 2})
      adapter.refresh_limits
      assert_equal 1, adapter.active_count("queued_test_job")

      adapter.finished_with(result1.not_nil!.job_run, result1.not_nil!.queue)
      assert_equal 0, adapter.active_count("queued_test_job")
    end
  end

  it "delegates finished_with to the inner adapter" do
    clean_slate do
      register QueuedTestJob
      2.times { QueuedTestJob.new.enqueue }

      adapter = Mosquito::RemoteConfigDequeueAdapter.new(
        defaults: {"queued_test_job" => 1},
        refresh_interval: 0.seconds,
      )

      result1 = adapter.dequeue(queue_list)
      refute_nil result1
      assert_equal 1, adapter.active_count("queued_test_job")

      # Blocked.
      result2 = adapter.dequeue(queue_list)
      assert_nil result2

      # Signal completion.
      adapter.finished_with(result1.not_nil!.job_run, result1.not_nil!.queue)
      assert_equal 0, adapter.active_count("queued_test_job")

      # Now a dequeue should succeed again.
      result3 = adapter.dequeue(queue_list)
      refute_nil result3
    end
  end

  it "can be used via the overseer" do
    clean_slate do
      adapter = Mosquito::RemoteConfigDequeueAdapter.new(
        defaults: {"queued_test_job" => 5},
        refresh_interval: 0.seconds,
      )
      overseer.dequeue_adapter = adapter

      register QueuedTestJob
      expected_job_run = QueuedTestJob.new.enqueue

      result = overseer.dequeue_job?
      refute_nil result
      if result
        assert_equal expected_job_run, result.job_run
      end
    end
  end

  describe "per-overseer configuration" do
    it "uses per-overseer limits when overseer_id is set" do
      clean_slate do
        register QueuedTestJob
        3.times { QueuedTestJob.new.enqueue }

        adapter = Mosquito::RemoteConfigDequeueAdapter.new(
          defaults: {"queued_test_job" => 3},
          overseer_id: "gpu-worker-1",
          refresh_interval: 0.seconds,
        )

        # Set a per-overseer limit of 1.
        Mosquito::RemoteConfigDequeueAdapter.store_limits(
          {"queued_test_job" => 1}, overseer_id: "gpu-worker-1"
        )

        result1 = adapter.dequeue(queue_list)
        refute_nil result1

        # Should be blocked by the per-overseer limit.
        result2 = adapter.dequeue(queue_list)
        assert_nil result2
      end
    end

    it "per-overseer limits override global limits" do
      clean_slate do
        adapter = Mosquito::RemoteConfigDequeueAdapter.new(
          defaults: {"queue_a" => 10},
          overseer_id: "gpu-worker-1",
          refresh_interval: 0.seconds,
        )

        # Global says 5, per-overseer says 2 — per-overseer wins.
        Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 5})
        Mosquito::RemoteConfigDequeueAdapter.store_limits(
          {"queue_a" => 2}, overseer_id: "gpu-worker-1"
        )

        adapter.refresh_limits
        assert_equal 2, adapter.limits["queue_a"]
      end
    end

    it "falls back to global when no per-overseer key exists" do
      clean_slate do
        adapter = Mosquito::RemoteConfigDequeueAdapter.new(
          defaults: {"queue_a" => 10},
          overseer_id: "gpu-worker-1",
          refresh_interval: 0.seconds,
        )

        Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 5})

        adapter.refresh_limits
        assert_equal 5, adapter.limits["queue_a"]
      end
    end

    it "merges defaults, global, and per-overseer layers" do
      clean_slate do
        adapter = Mosquito::RemoteConfigDequeueAdapter.new(
          defaults: {"queue_a" => 10, "queue_b" => 20, "queue_c" => 30},
          overseer_id: "gpu-worker-1",
          refresh_interval: 0.seconds,
        )

        # Global overrides queue_a and adds queue_d.
        Mosquito::RemoteConfigDequeueAdapter.store_limits({
          "queue_a" => 5,
          "queue_d" => 40,
        })

        # Per-overseer overrides queue_a again and queue_b.
        Mosquito::RemoteConfigDequeueAdapter.store_limits(
          {"queue_a" => 1, "queue_b" => 2},
          overseer_id: "gpu-worker-1"
        )

        adapter.refresh_limits

        assert_equal 1, adapter.limits["queue_a"]   # per-overseer wins
        assert_equal 2, adapter.limits["queue_b"]   # per-overseer wins
        assert_equal 30, adapter.limits["queue_c"]  # default (untouched)
        assert_equal 40, adapter.limits["queue_d"]  # global (no per-overseer)
      end
    end

    it "adapters without overseer_id ignore per-overseer keys" do
      clean_slate do
        adapter = Mosquito::RemoteConfigDequeueAdapter.new(
          defaults: {"queue_a" => 10},
          refresh_interval: 0.seconds,
        )

        Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 5})
        Mosquito::RemoteConfigDequeueAdapter.store_limits(
          {"queue_a" => 1}, overseer_id: "gpu-worker-1"
        )

        adapter.refresh_limits

        # Without an overseer_id, only global is used.
        assert_equal 5, adapter.limits["queue_a"]
      end
    end
  end

  describe "class-level storage helpers" do
    it "round-trips global limits through the backend" do
      clean_slate do
        limits = {"queue_a" => 3, "queue_b" => 7}
        Mosquito::RemoteConfigDequeueAdapter.store_limits(limits)

        retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits
        assert_equal 3, retrieved["queue_a"]
        assert_equal 7, retrieved["queue_b"]
      end
    end

    it "round-trips per-overseer limits through the backend" do
      clean_slate do
        limits = {"queue_a" => 1}
        Mosquito::RemoteConfigDequeueAdapter.store_limits(limits, overseer_id: "worker-2")

        retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits("worker-2")
        assert_equal 1, retrieved["queue_a"]

        # Global should be unaffected.
        global = Mosquito::RemoteConfigDequeueAdapter.stored_limits
        assert_equal({} of String => Int32, global)
      end
    end

    it "store_limits overwrites rather than merges (stale entries are removed)" do
      clean_slate do
        Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 3, "queue_b" => 7})
        Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 1})

        retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits
        assert_equal 1, retrieved["queue_a"]
        refute retrieved.has_key?("queue_b"), "queue_b should have been removed by the overwrite"
      end
    end

    it "store_limits with overseer_id overwrites rather than merges" do
      clean_slate do
        Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 3, "queue_b" => 7}, overseer_id: "worker-1")
        Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 1}, overseer_id: "worker-1")

        retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits("worker-1")
        assert_equal 1, retrieved["queue_a"]
        refute retrieved.has_key?("queue_b"), "queue_b should have been removed by the overwrite"
      end
    end

    it "store_limits with an empty hash removes all stored limits" do
      clean_slate do
        Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 3})
        Mosquito::RemoteConfigDequeueAdapter.store_limits({} of String => Int32)

        retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits
        assert_equal({} of String => Int32, retrieved)
      end
    end

    it "returns an empty hash when no limits are stored" do
      clean_slate do
        retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits
        assert_equal({} of String => Int32, retrieved)
      end
    end

    it "clear_limits removes global stored data" do
      clean_slate do
        Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 1})
        Mosquito::RemoteConfigDequeueAdapter.clear_limits

        retrieved = Mosquito::RemoteConfigDequeueAdapter.stored_limits
        assert_equal({} of String => Int32, retrieved)
      end
    end

    it "clear_limits with overseer_id removes only that overseer's data" do
      clean_slate do
        Mosquito::RemoteConfigDequeueAdapter.store_limits({"queue_a" => 5})
        Mosquito::RemoteConfigDequeueAdapter.store_limits(
          {"queue_a" => 1}, overseer_id: "worker-1"
        )

        Mosquito::RemoteConfigDequeueAdapter.clear_limits(overseer_id: "worker-1")

        # Per-overseer is gone.
        per_overseer = Mosquito::RemoteConfigDequeueAdapter.stored_limits("worker-1")
        assert_equal({} of String => Int32, per_overseer)

        # Global is still there.
        global = Mosquito::RemoteConfigDequeueAdapter.stored_limits
        assert_equal 5, global["queue_a"]
      end
    end
  end

  describe "Api integration" do
    it "reads and writes global limits through the Api module" do
      clean_slate do
        Mosquito::Api.set_concurrency_limits({"queue_x" => 10})
        result = Mosquito::Api.concurrency_limits
        assert_equal 10, result["queue_x"]
      end
    end

    it "reads and writes per-overseer limits through the Api module" do
      clean_slate do
        Mosquito::Api.set_concurrency_limits(
          {"queue_x" => 2}, overseer_id: "gpu-worker-1"
        )
        result = Mosquito::Api.concurrency_limits(overseer_id: "gpu-worker-1")
        assert_equal 2, result["queue_x"]

        # Global should be unaffected.
        global = Mosquito::Api.concurrency_limits
        assert_equal({} of String => Int32, global)
      end
    end
  end
end


================================================
FILE: spec/mosquito/dequeue_adapters/shuffle_dequeue_adapter_spec.cr
================================================
require "../../spec_helper"

describe "Mosquito::ShuffleDequeueAdapter" do
  getter(overseer : MockOverseer) { MockOverseer.new }
  getter(queue_list : MockQueueList) { overseer.queue_list.as(MockQueueList) }
  getter(executor : MockExecutor) { overseer.executors.first.as(MockExecutor) }

  def register(job_class : Mosquito::Job.class)
    Mosquito::Base.register_job_mapping job_class.name.underscore, job_class
    queue_list.discovered_queues << job_class.queue
  end

  it "is the default adapter" do
    assert_instance_of Mosquito::ShuffleDequeueAdapter, Mosquito.configuration.dequeue_adapter
  end

  it "dequeues a job from the queue list" do
    clean_slate do
      register QueuedTestJob
      expected_job_run = QueuedTestJob.new.enqueue

      adapter = Mosquito::ShuffleDequeueAdapter.new
      result = adapter.dequeue(queue_list)

      refute_nil result
      if result
        assert_equal expected_job_run, result.job_run
        assert_equal QueuedTestJob.queue, result.queue
      end
    end
  end

  it "returns nil when no jobs are available" do
    clean_slate do
      register QueuedTestJob

      adapter = Mosquito::ShuffleDequeueAdapter.new
      result = adapter.dequeue(queue_list)
      assert_nil result
    end
  end

  describe "custom adapter" do
    it "can be swapped on the overseer" do
      clean_slate do
        null_adapter = NullDequeueAdapter.new
        overseer.dequeue_adapter = null_adapter

        register QueuedTestJob
        QueuedTestJob.new.enqueue

        result = overseer.dequeue_job?
        assert_nil result
        assert_equal 1, null_adapter.dequeue_count
      end
    end

    it "receives the queue list when dequeuing" do
      clean_slate do
        spy_adapter = SpyDequeueAdapter.new
        overseer.dequeue_adapter = spy_adapter

        register QueuedTestJob
        queue_list.discovered_queues << Mosquito::Queue.new("extra_queue")

        overseer.dequeue_job?

        assert_includes spy_adapter.checked_queues, "queued_test_job"
        assert_includes spy_adapter.checked_queues, "extra_queue"
      end
    end
  end

  describe "overseer integration" do
    it "dequeue_job? delegates to the adapter" do
      clean_slate do
        register QueuedTestJob
        expected_job_run = QueuedTestJob.new.enqueue

        result = overseer.dequeue_job?
        refute_nil result
        if result
          assert_equal expected_job_run, result.job_run
        end
      end
    end
  end
end


================================================
FILE: spec/mosquito/dequeue_adapters/weighted_dequeue_adapter_spec.cr
================================================
require "../../spec_helper"

describe "Mosquito::WeightedDequeueAdapter" do
  getter(overseer : MockOverseer) { MockOverseer.new }
  getter(queue_list : MockQueueList) { overseer.queue_list.as(MockQueueList) }

  def register(job_class : Mosquito::Job.class)
    Mosquito::Base.register_job_mapping job_class.name.underscore, job_class
    queue_list.discovered_queues << job_class.queue
  end

  it "dequeues a job from a weighted queue" do
    clean_slate do
      register QueuedTestJob
      expected_job_run = QueuedTestJob.new.enqueue

      adapter = Mosquito::WeightedDequeueAdapter.new({
        "queued_test_job" => 5,
      })

      result = adapter.dequeue(queue_list)
      refute_nil result
      if result
        assert_equal expected_job_run, result.job_run
        assert_equal QueuedTestJob.queue, result.queue
      end
    end
  end

  it "returns nil when no jobs are available" do
    clean_slate do
      register QueuedTestJob

      adapter = Mosquito::WeightedDequeueAdapter.new({
        "queued_test_job" => 3,
      })

      result = adapter.dequeue(queue_list)
      assert_nil result
    end
  end

  it "assigns default weight of 1 to unconfigured queues" do
    clean_slate do
      register QueuedTestJob
      expected_job_run = QueuedTestJob.new.enqueue

      # No weight configured for queued_test_job — defaults to 1.
      adapter = Mosquito::WeightedDequeueAdapter.new({
        "other_queue" => 10,
      })

      result = adapter.dequeue(queue_list)
      refute_nil result
      if result
        assert_equal expected_job_run, result.job_run
      end
    end
  end

  it "higher-weight queues are dequeued more often" do
    clean_slate do
      register QueuedTestJob
      register EchoJob

      adapter = Mosquito::WeightedDequeueAdapter.new({
        "queued_test_job" => 10,
        "io_queue"        => 1,
      })

      # Enqueue enough jobs that neither queue drains during the sample.
      200.times { QueuedTestJob.new.enqueue }
      200.times { EchoJob.new(text: "hello").enqueue }

      dequeue_counts = Hash(String, Int32).new(0)

      # Sample 50 dequeues — well within the 200 available per queue.
      50.times do
        result = adapter.dequeue(queue_list)
        if result
          dequeue_counts[result.queue.name] = dequeue_counts[result.queue.name] + 1
        end
      end

      # With weights 10:1, the high-weight queue should be dequeued
      # significantly more often over a 50-dequeue sample.
      heavy_count = dequeue_counts.fetch("queued_test_job", 0)
      light_count = dequeue_counts.fetch("io_queue", 0)
      assert heavy_count > light_count, "Expected queued_test_job (#{heavy_count}) to be dequeued more than io_queue (#{light_count})"
    end
  end

  it "can be used via the overseer" do
    clean_slate do
      adapter = Mosquito::WeightedDequeueAdapter.new({
        "queued_test_job" => 5,
      })
      overseer.dequeue_adapter = adapter

      register QueuedTestJob
      expected_job_run = QueuedTestJob.new.enqueue

      result = overseer.dequeue_job?
      refute_nil result
      if result
        assert_equal expected_job_run, result.job_run
      end
    end
  end
end


================================================
FILE: spec/mosquito/exceptions_spec.cr
================================================
require "../spec_helper"

describe "Mosquito exceptions" do
  it "declares JobFailed" do
    Mosquito::JobFailed.new "test"
  end

  it "declares DoubleRun" do
    Mosquito::DoubleRun.new "test"
  end

  it "declares IrretrievableParameter" do
    Mosquito::IrretrievableParameter.new "test"
  end
end


================================================
FILE: spec/mosquito/job/job_state_spec.cr
================================================
require "../../spec_helper"

describe Mosquito::Job::State do
  describe "executed?" do
    it "Marks jobs as executed when they've either succeeded or failed" do
      assert Mosquito::Job::State::Succeeded.executed?
      assert Mosquito::Job::State::Failed.executed?
    end

    it "Doesn't mark jobs as executed in any other state" do
      refute Mosquito::Job::State::Initialization.executed?
      refute Mosquito::Job::State::Running.executed?
      refute Mosquito::Job::State::Aborted.executed?
      refute Mosquito::Job::State::Preempted.executed?
    end
  end
end


================================================
FILE: spec/mosquito/job_run/rescheduling_spec.cr
================================================
require "../../spec_helper"

describe "job_run rescheduling" do
  @failing_job_run : Mosquito::JobRun?
  getter failing_job_run : Mosquito::JobRun { create_job_run "failing_job" }

  it "calculates reschedule interval correctly" do
    intervals = {
      1 => 2,
      2 => 8,
      3 => 18,
      4 => 32
    }

    intervals.each do |count, delay|
      job_run = Mosquito::JobRun.retrieve(failing_job_run.id.not_nil!).not_nil!
      job_run.run
      assert_equal delay.seconds, job_run.reschedule_interval
    end
  end

  it "prevents rescheduling a job too many times" do
    run_job_run = -> do
      job_run = Mosquito::JobRun.retrieve(failing_job_run.id.not_nil!).not_nil!
      job_run.run
      job_run
    end

    max_reschedules = 4
    max_reschedules.times do
      job_run = run_job_run.call
      assert job_run.rescheduleable?
    end

    job_run = run_job_run.call
    refute job_run.rescheduleable?
  end

  it "counts retries upon failure" do
    assert_equal 0, failing_job_run.retry_count
    failing_job_run.run
    assert_equal 1, failing_job_run.retry_count
  end

  it "updates the backend when a failure happens" do
    failing_job_run.run
    saved_job_run = Mosquito::JobRun.retrieve failing_job_run.id.not_nil!
    assert_equal 1, saved_job_run.not_nil!.retry_count
  end

  it "does not reschedule a job which fails with retry=false" do
    job = FailingJob.new
    job.fail_with_retry = false
    job.run

    refute job.should_retry
  end

  describe "preempted jobs" do
    it "sets state to preempted and does not execute" do
      job = PreemptingJob.new
      job.run
      assert job.preempted?
      refute job.executed?
    end

    it "uses normal backoff when preempted without an until time" do
      job = PreemptingJob.new
      job.run
      assert_equal 2.seconds, job.reschedule_interval(1)
      assert_equal 8.seconds, job.reschedule_interval(2)
    end

    it "uses the until time for reschedule interval when provided" do
      Timecop.freeze(Time.utc) do
        future = Time.utc + 30.seconds
        job = PreemptingJob.new
        job.preempt_until = future
        job.run

        interval = job.reschedule_interval(1)
        assert_equal 30.seconds, interval
      end
    end

    it "falls back to normal backoff when until time is in the past" do
      Timecop.freeze(Time.utc) do
        past = Time.utc - 5.seconds
        job = PreemptingJob.new
        job.preempt_until = past
        job.run

        assert_equal 2.seconds, job.reschedule_interval(1)
      end
    end

    it "respects rescheduleable? override when preempted" do
      job = NonReschedulablePreemptingJob.new
      job.run
      assert job.preempted?
      refute job.rescheduleable?(0)
    end
  end
end


================================================
FILE: spec/mosquito/job_run/running_spec.cr
================================================
require "../../spec_helper"

describe "job_run running" do
  # the job run timestamps are stored as a unix epoch with millis, so nanosecond precision is lost.
  def at_beginning_of_millisecond(time)
    time - (time.nanosecond.nanoseconds) + (time.millisecond.milliseconds)
  end

  it "uses the lookup table to build a job" do
    job_instance = create_job_run.build_job
    assert_instance_of JobWithConfig, job_instance
  end

  it "populates the variables of a job" do
    job_instance = create_job_run.build_job

    assert_instance_of JobWithConfig, job_instance
    assert_equal job_run_config, job_instance.as(JobWithConfig).config
  end

  it "runs the job" do
    JobWithPerformanceCounter.reset_performance_counter!
    create_job_run("job_with_performance_counter").run
    assert_equal 1, JobWithPerformanceCounter.performances
  end

  it "sets started_at when a job is run" do
    now = at_beginning_of_millisecond Time.utc
    job_run = create_job_run
    Timecop.freeze now do
      job_run.run
    end
    assert_equal now, job_run.started_at
  end

  it "sets finished_at when a job is run" do
    now = at_beginning_of_millisecond Time.utc
    job_run = create_job_run
    Timecop.freeze now do
      job_run.run
    end
    assert_equal now, job_run.finished_at
  end

  it "has nil timestamps before a job is run" do
    job_run = create_job_run
    assert_nil job_run.started_at
    assert_nil job_run.finished_at
  end
end


================================================
FILE: spec/mosquito/job_run/storage_spec.cr
================================================
require "../../spec_helper"

describe "job_run storage" do
  getter backend : Mosquito::Backend::Queue = Mosquito.backend.queue("testing")

  getter config = {
    "year" => "1752",
    "name" => "the year september lost 12 days"
  }

  getter job_run : Mosquito::JobRun do
    Mosquito::JobRun.new("mock_job_run").tap do |job_run|
      job_run.config = config
      job_run.store
    end
  end

  it "builds the backend key correctly" do
    assert_equal "mosquito:job_run:1", Mosquito::JobRun.config_key "1"
    assert_equal "mosquito:job_run:#{job_run.id}", job_run.config_key
  end

  it "can store and retrieve a job_run with attributes" do
    stored_job_run = Mosquito::JobRun.retrieve job_run.id
    if stored_job_run
      assert_equal config, stored_job_run.config
    else
      flunk "Could not retrieve job_run"
    end
  end

  it "stores job_runs in the backend" do
    stored_job_run = backend.backend.retrieve Mosquito::JobRun.config_key(job_run.id)
    stored_config = stored_job_run.reject! %w|type enqueue_time retry_count|
    assert_equal config, stored_config
  end

  it "can delete a job_run" do
    job_run.delete
    saved_config = backend.backend.retrieve job_run.config_key
    assert_empty saved_config
  end

  it "can set a timed delete on a job_run" do
    ttl = 10
    job_run.delete(in: ttl)
    set_ttl = backend.backend.expires_in job_run.config_key
    assert_equal ttl, set_ttl
  end

  it "can reload a job_run" do
    job_run.reload
  end

  describe "timestamp retrieval" do
    # the job run timestamps are stored as a unix epoch with millis, so nanosecond precision is lost.
    def at_beginning_of_millisecond(time)
      time - (time.nanosecond.nanoseconds) + (time.millisecond.milliseconds)
    end

    it "retrieves started_at and finished_at timestamps" do
      now = at_beginning_of_millisecond Time.utc
      job_run = create_job_run
      Timecop.freeze now do
        job_run.run
      end

      retrieved = Mosquito::JobRun.retrieve job_run.id
      if retrieved
        assert_equal now, retrieved.started_at
        assert_equal now, retrieved.finished_at
      else
        flunk "Could not retrieve job_run"
      end
    end

    it "does not include timestamps in config after retrieve" do
      job_run = create_job_run
      job_run.run

      retrieved = Mosquito::JobRun.retrieve job_run.id
      if retrieved
        refute retrieved.config.has_key?("started_at")
        refute retrieved.config.has_key?("finished_at")
      else
        flunk "Could not retrieve job_run"
      end
    end

    it "retrieves nil timestamps for unexecuted job runs" do
      retrieved = Mosquito::JobRun.retrieve job_run.id
      if retrieved
        assert_nil retrieved.started_at
        assert_nil retrieved.finished_at
      else
        flunk "Could not retrieve job_run"
      end
    end
  end

  it "persists overseer_id via claimed_by and retrieves it" do
    test_overseer = MockOverseer.new
    job_run.claimed_by test_overseer
    retrieved = Mosquito::JobRun.retrieve job_run.id
    assert retrieved
    assert_equal test_overseer.observer.instance_id, retrieved.not_nil!.overseer_id
  end

  it "round-trips overseer_id through store and retrieve" do
    test_overseer = MockOverseer.new
    job_run.claimed_by test_overseer
    job_run.store

    retrieved = Mosquito::JobRun.retrieve job_run.id
    assert retrieved
    assert_equal test_overseer.observer.instance_id, retrieved.not_nil!.overseer_id
  end
end


================================================
FILE: spec/mosquito/job_run_spec.cr
================================================
require "../spec_helper"
require "./job_run/*"


================================================
FILE: spec/mosquito/job_spec.cr
================================================
require "../spec_helper"

describe Mosquito::Job do
  getter(passing_job) { PassingJob.new }
  getter(failing_job) { FailingJob.new }
  getter(not_implemented_job) { NotImplementedJob.new }

  getter(throttled_job) { ThrottledJob.new }
  getter(hooked_job) { JobWithHooks.new }

  describe "run" do
    it "captures JobFailed and marks sucess=false" do
      failing_job.run
      assert failing_job.failed?
    end

    it "sets #executed? and #succeeded?" do
      refute passing_job.executed?

      passing_job.run

      assert passing_job.executed?
      assert passing_job.succeeded?
    end

    it "emits a failure message when #fail contains a reason message" do
      clear_logs

      failing_job.run
      assert failing_job.failed?

      assert_logs_match failing_job.exception_message
    end

    it "exception messages are sent to the logs" do
      clear_logs

      failing_job.fail_with_exception = true
      failing_job.run
      assert failing_job.failed?

      assert_logs_match failing_job.exception_message
    end

    it "captures and marks failure for other exceptions" do
      clear_logs

      assert_nil failing_job.exception

      failing_job.fail_with_exception = true
      failing_job.run
      assert failing_job.failed?
      refute_nil failing_job.exception
    end

    it "sets success=false when #fail-ed" do
      failing_job.run
      refute failing_job.succeeded?
    end

    it "fails when no perform is implemented, and a messsage is sent to the logs" do
      clear_logs

      not_implemented_job.run
      assert not_implemented_job.failed?

      assert_logs_match "No job definition found"
    end
  end

  it "fetches the default queue" do
    assert_equal "passing_job", PassingJob.queue.name
  end

  it "fetches the named queue" do
    assert_equal "io_queue", EchoJob.queue.name
  end

  describe "reschedule interval" do
    it "calculates reschedule interval correctly" do
      intervals = {
        1 => 2,
        2 => 8,
        3 => 18,
        4 => 32
      }

      intervals.each do |count, delay|
        assert_equal delay.seconds, passing_job.reschedule_interval(count)
      end
    end


    it "allows overriding the reschedule interval" do
      intervals = 1..4

      intervals.each do |count|
        assert_equal 4.seconds, CustomRescheduleIntervalJob.new.reschedule_interval(count)
      end
    end
  end

  describe "metadata" do
    it "returns a metadata instance" do
      assert_instance_of Mosquito::Metadata, passing_job.metadata
    end

    it "is a memoized instance" do
      one = passing_job.metadata
      two = passing_job.metadata

      assert_same one, two
    end
  end

  describe "self.metadata" do
    it "returns a metadata instance" do
      assert PassingJob.metadata.is_a?(Mosquito::Metadata)
    end

    it "is readonly" do
      metadata = PassingJob.metadata
      assert metadata.readonly?
    end
  end

  describe "self.metadata_key" do
    it "includes the class name" do
      assert_includes PassingJob.metadata_key, "passing_job"
    end
  end

  describe "before_hooks" do
    it "should execute hooks" do
      clear_logs
      hooked_job.should_fail = false
      hooked_job.run
      assert_logs_match "Before Hook Executed"
      assert_logs_match "2nd Before Hook Executed"
      assert_logs_match "Perform Executed"
    end

    it "should not exec when a before hook fails the job" do
      clear_logs
      hooked_job.should_fail = true
      hooked_job.run

      assert_logs_match "Before Hook Executed"
      assert_logs_match "2nd Before Hook Executed"
      refute_logs_match "Perform Executed"
    end
  end

  describe "after_hooks" do
    it "should execute `after` hooks" do
      clear_logs
      hooked_job.should_fail = false
      hooked_job.run
      assert_logs_match "After Hook Executed"
      assert_logs_match "2nd After Hook Executed"
      assert_logs_match "Perform Executed"
    end

    it "should run the `after` hooks even if a job fails" do
      clear_logs
      hooked_job.should_fail = true
      hooked_job.run
      assert_logs_match "After Hook Executed"
      assert_logs_match "2nd After Hook Executed"
      refute_logs_match "Perform Executed"
    end
  end
end


================================================
FILE: spec/mosquito/key_builder_spec.cr
================================================
require "../spec_helper"

describe Mosquito::KeyBuilder do
  it "builds keys from tuples" do
    assert_equal "fizz:buzz", KeyBuilder.build({:fizz, :buzz})
  end

  it "builds keys from strings" do
    assert_equal "fizz:buzz", KeyBuilder.build("fizz", "buzz")
  end

  it "builds keys from an array" do
    assert_equal "fizz:buzz", KeyBuilder.build(["fizz", "buzz"])
  end

  it "builds keys from integers" do
    assert_equal "fizz:6", KeyBuilder.build("fizz", 6)
  end

  it "builds keys from floats" do
    assert_equal "2.4:buzz", KeyBuilder.build(2.4, "buzz")
  end
end


================================================
FILE: spec/mosquito/metadata_spec.cr
================================================
require "../spec_helper"

describe Mosquito::Metadata do
  getter(store_name : String) { "test_store#{rand 1000}" }
  getter(store : Metadata) { Metadata.new store_name }
  getter(field : String) { "foo#{rand 1000}" }

  it "increments" do
    clean_slate do
      store.increment field
      value = store[field]?
      assert_equal "1", value

      store.increment field
      value = store[field]?
      assert_equal "2", value
    end
  end

  it "increments with a configurable amount" do
    clean_slate do
      store.increment field
      value = store[field]?.not_nil!
      assert_equal "1", value

      delta = 2
      store.increment field, by: delta
      new_value = store[field]?.not_nil!
      assert_equal delta, (new_value.to_i - value.to_i)
    end
  end

  it "decrements" do
    clean_slate do
      store.decrement field
      value = store[field]?
      assert_equal "-1", value

      store.decrement field
      value = store[field]?
      assert_equal "-2", value
    end
  end

  it "dumps to a hash" do
    clean_slate do
      expected = { "one" => "1", "two" => "2", "three" => "3" }

      expected.each { |key, value| store[key] = value }

      assert_equal expected, store.to_h
    end
  end

  it "can be readonly" do
    clean_slate do
      store[field] = "truth"
      readonly_store = Metadata.new store_name, readonly: true
      assert_equal "truth", readonly_store[field]?

      assert_raises RuntimeError do
        readonly_store[field] = "lies"
      end
    end
  end

  it "can set and read a value" do
    clean_slate do
      store[field] = "truth"
      assert_equal "truth", store[field]?
    end
  end

  describe "with a hash" do
    it "can set and read a hash" do
      clean_slate do
        store.set({"one" => "1", "two" => "2", "three" => "3"})
        assert_equal "1", store["one"]?
        assert_equal "2", store["two"]?
        assert_equal "3", store["three"]?
      end
    end

    it "can set a hash and delete a value from the hash" do
      clean_slate do
        store.set({"one" => "1", "two" => "2", "three" => "3"})
        store.set({"two" => nil, "six" => "6"})
        assert_equal "1", store["one"]?
        assert_equal nil, store["two"]?
        assert_equal "3", store["three"]?
        assert_equal "6", store["six"]?
      end
    end

    it "can store string-only values" do
      clean_slate do
        values = {"one" => "1", "two" => "2", "three" => "3"}
        store.set(values)
        assert_equal "1", store["one"]?
        assert_equal "2", store["two"]?
        assert_equal "3", store["three"]?
        assert_equal values, store.to_h
      end
    end
  end

  it "can be deleted" do
    clean_slate do
      store[field] = "truth"
      assert_equal "truth", store[field]?
      store.delete
      assert_equal nil, Metadata.new(store_name)[field]?
    end
  end

  it "can be deleted with a ttl" do
    clean_slate do
      store[field] = "truth"
      assert_equal "truth", store[field]?
      store.delete(in: 1.minute)
      assert_in_epsilon(60, Mosquito.backend.expires_in(store_name))
      store.delete
    end
  end
end


================================================
FILE: spec/mosquito/periodic_job_run_spec.cr
================================================
require "../spec_helper"

describe Mosquito::PeriodicJobRun do
  getter interval : Time::Span = 2.minutes

  it "tries to execute but fails before the interval has passed" do
    now = Time.utc.at_beginning_of_second
    job_run = PeriodicJobRun.new PeriodicTestJob, interval
    job_run.last_executed_at = now

    Timecop.freeze(now + 1.minute) do
      job_run.try_to_execute
      assert_equal now, job_run.last_executed_at
    end
  end

  it "executes" do
    now = Time.utc.at_beginning_of_second
    job_run = PeriodicJobRun.new PeriodicTestJob, interval
    job_run.last_executed_at = now

    Timecop.freeze(now + interval) do
      job_run.try_to_execute
      assert_equal now + interval, job_run.last_executed_at
    end
  end

  it "checks the metadata store for the last executed timestamp" do
    now = Time.utc.at_beginning_of_second
    clean_slate do
      job_run = PeriodicJobRun.new PeriodicTestJob, interval
      job_run.last_executed_at = now - 1.minute

      Timecop.freeze(now) do
        another_job_run = PeriodicJobRun.new PeriodicTestJob, interval
        refute another_job_run.try_to_execute
      end
    end
  end

  it "does not enqueue a second job run when one is already pending" do
    clean_slate do
      now = Time.utc.at_beginning_of_second
      periodic = PeriodicJobRun.new PeriodicTestJob, interval

      # First execution should enqueue.
      Timecop.freeze(now) do
        periodic.last_executed_at = now - interval
        assert periodic.try_to_execute
      end

      queue = PeriodicTestJob.queue
      first_size = queue.size(include_dead: false)
      assert first_size > 0, "Expected at least one job in the queue"

      # Second execution after another interval should be skipped
      # because the first job run hasn't finished yet.
      Timecop.freeze(now + interval) do
        assert periodic.try_to_execute
      end

      second_size = queue.size(include_dead: false)
      assert_equal first_size, second_size
    end
  end

  it "enqueues again after the pending job run finishes" do
    clean_slate do
      now = Time.utc.at_beginning_of_second
      periodic = PeriodicJobRun.new PeriodicTestJob, interval

      # Enqueue the first job run.
      Timecop.freeze(now) do
        periodic.last_executed_at = now - interval
        periodic.try_to_execute
      end

      # Simulate the job finishing by writing finished_at to the backend.
      pending_id = periodic.metadata["pending_run_id"]?
      refute_nil pending_id
      Mosquito.backend.set(
        Mosquito::JobRun.config_key(pending_id.not_nil!),
        "finished_at",
        Time.utc.to_unix_ms.to_s
      )

      queue = PeriodicTestJob.queue
      size_after_first = queue.size(include_dead: false)

      # Now a new interval passes — should enqueue since the previous one finished.
      Timecop.freeze(now + interval) do
        assert periodic.try_to_execute
      end

      size_after_second = queue.size(include_dead: false)
      assert size_after_second > size_after_first
    end
  end

  it "enqueues again when the pending job run config has been cleaned up" do
    clean_slate do
      now = Time.utc.at_beginning_of_second
      periodic = PeriodicJobRun.new PeriodicTestJob, interval

      # Enqueue the first job run.
      Timecop.freeze(now) do
        periodic.last_executed_at = now - interval
        periodic.try_to_execute
      end

      pending_id = periodic.metadata["pending_run_id"]?
      refute_nil pending_id

      # Simulate the job run config being deleted (e.g. TTL expiry).
      Mosquito.backend.delete Mosquito::JobRun.config_key(pending_id.not_nil!)

      queue = PeriodicTestJob.queue
      size_before = queue.size(include_dead: false)

      # Next interval should enqueue because the old run is gone.
      Timecop.freeze(now + interval) do
        assert periodic.try_to_execute
      end

      size_after = queue.size(include_dead: false)
      assert size_after > size_before
    end
  end
end


================================================
FILE: spec/mosquito/periodic_job_spec.cr
================================================
require "../spec_helper"

describe Mosquito::PeriodicJob do
  getter(runner) { Mosquito::TestableRunner.new }

  it "correctly renders job_type" do
    assert_equal "periodic_test_job", PeriodicTestJob.job_type
  end

  it "builds a job_run" do
    job = PeriodicTestJob.new
    job_run = job.build_job_run

    assert_instance_of JobRun, job_run
    assert_equal PeriodicTestJob.job_type, job_run.type
  end

  it "is not reschedulable" do
    refute PeriodicTestJob.new.rescheduleable?
  end

  it "registers in job mapping" do
    assert_equal PeriodicTestJob, Base.job_for_type(PeriodicTestJob.job_type)
  end

  it "can be scheduled at a MonthSpan interval" do
    clean_slate do
      Mosquito::Base.register_job_mapping MonthlyJob.queue.name, MonthlyJob
      Mosquito::Base.register_job_interval MonthlyJob, interval: 1.month
    end
  end

  it "schedules itself for an interval" do
    clean_slate do
      PeriodicTestJob.run_every 2.minutes
      scheduled_job_run = Base.scheduled_job_runs.first
      assert_equal PeriodicTestJob, scheduled_job_run.class
      assert_equal 2.minutes, scheduled_job_run.interval
    end
  end
end


================================================
FILE: spec/mosquito/queue_spec.cr
================================================
require "../spec_helper"

describe Queue do
  getter(name) { "test#{rand(1000)}" }

  getter(test_queue) do
    Mosquito::Queue.new(name)
  end

  @job_run : Mosquito::JobRun?
  getter(job_run) do
    Mosquito::JobRun.new("mock_job_run").tap(&.store)
  end

  getter backend : Mosquito::Backend::Queue do
    TestHelpers.backend.queue name
  end

  describe "config_key" do
    it "defaults to name" do
      name = "random_name"
      assert_equal name, Mosquito::Queue.new(name).config_key
    end
  end

  describe "flush" do
    it "purges all of the queue entries" do
      job_runs = (1..4).map do
        Mosquito::JobRun.new("mock_job_run").tap do |job_run|
          job_run.store
          test_queue.enqueue job_run
        end
      end

      assert_equal job_runs.size, test_queue.size
      test_queue.flush
      assert_equal 0, test_queue.size
    end
  end

  describe "enqueue" do
    it "adds the queue name to the list of queues" do
      clean_slate do
        test_queue.enqueue job_run
        assert_includes Mosquito.backend.list_queues, test_queue.name
      end
    end

    it "can enqueue a job_run for immediate processing" do
      clean_slate do
        test_queue.enqueue job_run
        job_run_ids = backend.list_waiting
        assert_includes job_run_ids, job_run.id
      end
    end

    it "can enqueue a job_run with a relative time" do
      Timecop.freeze(Time.utc) do
        clean_slate do
          offset = 3.seconds
          timestamp = offset.from_now
          test_queue.enqueue job_run, in: offset

          stored_time = backend.scheduled_job_run_time job_run
          assert_equal Time.unix_ms(timestamp.to_unix_ms), stored_time
        end
      end
    end

    it "can enqueue a job_run at a specific time" do
      Timecop.freeze(Time.utc) do
        clean_slate do
          timestamp = 3.seconds.from_now
          test_queue.enqueue job_run, at: timestamp
          stored_time = backend.scheduled_job_run_time job_run
          assert_equal Time.unix_ms(timestamp.to_unix_ms), stored_time
        end
      end
    end
  end

  describe "dequeue" do
    it "moves a job_run from waiting to pending on dequeue" do
      test_queue.enqueue job_run
      stored_job_run = test_queue.dequeue

      assert_equal job_run.id, stored_job_run.not_nil!.id

      pending_job_runs = backend.list_pending
      assert_includes pending_job_runs, job_run.id
    end

    it "dequeues job_runs which have been scheduled for a time that has passed" do
      job_run1 = job_run
      job_run2 = Mosquito::JobRun.new("mock_job_run").tap do |job_run|
        job_run.store
      end

      Timecop.freeze(Time.utc) do
        past = 1.minute.ago
        future = 1.minute.from_now
        test_queue.enqueue job_run1, at: past
        test_queue.enqueue job_run2, at: future
      end

      # check to make sure only job_run1 was dequeued
      overdue_job_runs = test_queue.dequeue_scheduled
      assert_equal 1, overdue_job_runs.size
      assert_equal job_run1.id, overdue_job_runs.first.id

      # check to make sure job_run2 is still scheduled
      scheduled_job_runs = backend.list_scheduled
      refute_includes scheduled_job_runs, job_run1.id
      assert_includes scheduled_job_runs, job_run2.id
    end
  end

  it "can forget about a pending job_run" do
    test_queue.enqueue job_run
    test_queue.dequeue
    pending_job_runs = backend.list_pending
    assert_includes pending_job_runs, job_run.id

    test_queue.forget job_run
    pending_job_runs = backend.list_pending
    refute_includes pending_job_runs, job_run.id
  end

  describe "banish" do
    it "can banish a pending job_run, adding it to the dead q" do
      test_queue.enqueue job_run
      test_queue.dequeue
      pending_job_runs = backend.list_pending
      assert_includes pending_job_runs, job_run.id

      test_queue.banish job_run
      pending_job_runs = backend.list_pending
      refute_includes pending_job_runs, job_run.id

      dead_job_runs = backend.list_dead
      assert_includes dead_job_runs, job_run.id
    end
  end

  describe "pause" do
    it "is not paused by default" do
      refute test_queue.paused?
    end

    it "can be paused" do
      test_queue.pause
      assert test_queue.paused?
    end

    it "can be resumed" do
      test_queue.pause
      assert test_queue.paused?
      test_queue.resume
      refute test_queue.paused?
    end

    it "prevents dequeue when paused" do
      test_queue.enqueue job_run
      test_queue.pause

      result = test_queue.dequeue
      assert_nil result

      # job_run should still be in waiting, not moved to pending
      waiting_job_runs = backend.list_waiting
      assert_includes waiting_job_runs, job_run.id
      pending_job_runs = backend.list_pending
      refute_includes pending_job_runs, job_run.id
    end

    it "allows dequeue after resume" do
      test_queue.enqueue job_run
      test_queue.pause
      assert_nil test_queue.dequeue

      test_queue.resume
      stored_job_run = test_queue.dequeue
      assert_equal job_run.id, stored_job_run.not_nil!.id
    end

    it "still allows enqueue while paused" do
      test_queue.pause
      test_queue.enqueue job_run
      waiting_job_runs = backend.list_waiting
      assert_includes waiting_job_runs, job_run.id
    end

    it "can be paused with a duration" do
      test_queue.pause for: 60.seconds
      assert test_queue.paused?
    end

    it "does not affect other queues" do
      other_queue = Mosquito::Queue.new("other_#{name}")
      other_job_run = Mosquito::JobRun.new("mock_job_run").tap(&.store)

      test_queue.pause
      other_queue.enqueue other_job_run

      assert_nil test_queue.dequeue
      stored = other_queue.dequeue
      assert_equal other_job_run.id, stored.not_nil!.id
    end
  end

end


================================================
FILE: spec/mosquito/queued_job_spec.cr
================================================
require "../spec_helper"

describe Mosquito::QueuedJob do
  getter(runner) { Mosquito::TestableRunner.new }
  getter(name) { "test#{rand(1000)}" }
  getter(job : QueuedTestJob) { QueuedTestJob.new }
  getter(queue : Queue) { QueuedTestJob.queue }
  getter(queue_hooked_job : QueueHookedTestJob) { QueueHookedTestJob.new }

  describe "enqueue" do
    it "enqueues" do
      clean_slate do
        job_run = job.enqueue
        enqueued = queue.backend.list_waiting
        assert_equal [job_run.id], enqueued
      end
    end

    it "enqueues with a delay" do
      clean_slate do
        job_run = job.enqueue in: 1.minute
        enqueued = queue.backend.list_scheduled
        assert_equal [job_run.id], enqueued
      end
    end

    it "enqueues with a target time" do
      clean_slate do
        job_run = job.enqueue at: 1.minute.from_now
        enqueued = queue.backend.list_scheduled
        assert_equal [job_run.id], enqueued
      end
    end

    it "fires before_enqueue_hook" do
      clean_slate do
        job_run = queue_hooked_job.enqueue
        assert queue_hooked_job.before_hook_ran
      end
    end

    it "doesnt enqueue if before_enqueue_hook fails" do
      clean_slate do
        queue_hooked_job.fail_before_hook = true
        job_run = queue_hooked_job.enqueue
        waiting_q = queue.backend.list_waiting
        assert_empty waiting_q
      end
    end

    it "fires after_enqueue_hook" do
      clean_slate do
        job_run = queue_hooked_job.enqueue
        assert queue_hooked_job.after_hook_ran
      end
    end

    it "passes the job config to the before_enqueue_hook" do
      clean_slate do
        job_run = queue_hooked_job.enqueue
        assert_equal job_run, queue_hooked_job.passed_job_config
      end
    end

    it "passes the job config to the after_enqueue_hook" do
      clean_slate do
        job_run = queue_hooked_job.enqueue
        assert_equal job_run, queue_hooked_job.passed_job_config
      end
    end
  end

  describe "parameters" do
    it "can be passed in" do
      clear_logs
      EchoJob.new("quack").perform
      assert_logs_match "quack"
    end

    it "can have a boolean false passed as a parameter (and it's not assumed to be a nil)" do
      clear_logs
      JobWithHooks.new(false).perform
      assert_includes logs, "Perform Executed"
    end

    it "can be omitted" do
      clean_slate do
        clear_logs
        job = JobWithNoParams.new.perform
        assert_includes logs, "no param job performed"
      end
    end
  end
end


================================================
FILE: spec/mosquito/rate_limiter_spec.cr
================================================
require "../spec_helper"

describe Mosquito::RateLimiter do
  describe "RateLimiter.rate_limit_stats" do
    it "provides the state and configuration of the limiter" do
      clean_slate do
        stats = RateLimitedJob.rate_limit_stats
        assert stats.has_key? :interval
        assert stats.has_key? :key
        assert stats.has_key? :increment
        assert stats.has_key? :limit
        assert stats.has_key? :window_start
        assert stats.has_key? :run_count
      end
    end

    it "defaults the window_start" do
      clean_slate do
        assert_equal Time::UNIX_EPOCH, RateLimitedJob.rate_limit_stats[:window_start]

        now = Time.utc.at_beginning_of_second
        RateLimitedJob.metadata["window_start"] = now.to_unix.to_s
        assert_equal now, RateLimitedJob.rate_limit_stats[:window_start]
      end
    end

    it "defaults the run_count" do
      clean_slate do
        assert_equal 0, RateLimitedJob.rate_limit_stats[:run_count]

        run_count = 27
        RateLimitedJob.metadata["run_count"] = run_count.to_s
        assert_equal run_count, RateLimitedJob.rate_limit_stats[:run_count]
      end
    end
  end

  describe "RateLimiter.metadata" do
    it "provides an instance of the metadata store" do
      assert_instance_of Metadata, RateLimitedJob.metadata
    end
  end

  describe "RateLimiter.rate_limit_key" do
    it "provides the metadata key for this class" do
      assert_equal "mosquito:rate_limit:rate_limit", RateLimitedJob.rate_limit_key
    end
  end

  describe "job counting" do
    it "increments the count when a job is run" do
      clean_slate do
        RateLimitedJob.new.run
        count = RateLimitedJob.metadata["run_count"]?.not_nil!.to_i

        RateLimitedJob.new.run
        new_count = RateLimitedJob.metadata["run_count"]?.not_nil!.to_i
        assert_equal 1, new_count - count
      end
    end

    it "doesnt increment the count when a job is not run" do
      clean_slate do
        RateLimitedJob.new(should_fail: false).run
        count = RateLimitedJob.metadata["run_count"]?.not_nil!.to_i

        RateLimitedJob.new(should_fail: true).run
        new_count = RateLimitedJob.metadata["run_count"]?.not_nil!.to_i
        assert_equal count, new_count
      end
    end

    it "increments the count by a configurable number" do
      clean_slate do
        delta = 2
        RateLimitedJob.new.run
        count = RateLimitedJob.metadata["run_count"]?.not_nil!.to_i

        RateLimitedJob.new(increment: delta).run
        new_count = RateLimitedJob.metadata["run_count"]?.not_nil!.to_i
        assert_equal delta, new_count - count
      end
    end

    it "resets the count when the window is over" do
      clean_slate do
        metadata = RateLimitedJob.metadata
        metadata["run_count"] = "45"
        metadata["window_start"] = Time::UNIX_EPOCH.to_unix.to_s
        RateLimitedJob.new.run
        count = RateLimitedJob.metadata["run_count"]?
        assert_equal "1", count
      end
    end

    it "counts multiple jobs with the same key in the same bucket" do
      clean_slate do
        metadata = RateLimitedJob.metadata
        metadata["window_start"] = Time.utc.to_unix.to_s

        RateLimitedJob.new.run
        count = RateLimitedJob.metadata["run_count"]?.not_nil!.to_i

        SecondRateLimitedJob.new.run
        new_count = RateLimitedJob.metadata["run_count"]?.not_nil!.to_i

        assert_equal RateLimitedJob.rate_limit_key, SecondRateLimitedJob.rate_limit_key
        assert_equal 1, new_count - count
      end
    end
  end

  describe "job preempting" do
    it "doesnt prevent excution if the rate limit count is less than zero" do
      metadata = RateLimitedJob.metadata
      metadata["run_count"] = "-1"
      metadata["window_start"] = Time.utc.to_unix.to_s
      job = RateLimitedJob.new
      job.run
      assert job.executed?
    end

    it "prevents a job from executing when the limit is reached" do
      metadata = RateLimitedJob.metadata
      metadata["run_count"] = Int32::MAX.to_s
      metadata["window_start"] = Time.utc.to_unix.to_s
      job = RateLimitedJob.new
      job.run
      refute job.executed?
      assert job.preempted?
    end

    it "allows a job to execute when the limit hasn't been reached" do
      metadata = RateLimitedJob.metadata
      metadata["window_start"] = Time.utc.to_unix.to_s
      metadata["run_count"] = "3"
      job = RateLimitedJob.new
      job.run
      assert job.executed?
    end

    it "allows a job to execute when the limit has been reached but the window is over" do
      metadata = RateLimitedJob.metadata
      metadata["run_count"] = Int32::MAX.to_s
      metadata["window_start"] = Time::UNIX_EPOCH.to_unix.to_s
      job = RateLimitedJob.new
      job.run
      assert job.executed?
    end
  end
end


================================================
FILE: spec/mosquito/resource_gate_spec.cr
================================================
require "../spec_helper"

describe "Mosquito::OpenGate" do
  it "always allows" do
    gate = Mosquito::OpenGate.new
    assert gate.allow?
  end
end

describe "Mosquito::ThresholdGate" do
  it "allows when metric is below threshold" do
    gate = Mosquito::ThresholdGate.new(threshold: 80.0, sample_ttl: 0.seconds) { 50.0 }
    assert gate.allow?
  end

  it "blocks when metric is at or above threshold" do
    gate = Mosquito::ThresholdGate.new(threshold: 80.0, sample_ttl: 0.seconds) { 85.0 }
    refute gate.allow?
  end

  it "blocks when metric equals threshold" do
    gate = Mosquito::ThresholdGate.new(threshold: 80.0, sample_ttl: 0.seconds) { 80.0 }
    refute gate.allow?
  end
end

describe "Mosquito::ResourceGate caching" do
  it "caches the check result within TTL" do
    call_count = 0
    gate = Mosquito::ThresholdGate.new(threshold: 80.0, sample_ttl: 5.seconds) do
      call_count += 1
      50.0
    end

    now = Time.utc
    Timecop.freeze(now) do
      gate.allow?
      gate.allow?
      gate.allow?
      assert_equal 1, call_count
    end
  end

  it "re-checks after TTL expires" do
    call_count = 0
    gate = Mosquito::ThresholdGate.new(threshold: 80.0, sample_ttl: 5.seconds) do
      call_count += 1
      50.0
    end

    now = Time.utc
    Timecop.freeze(now) do
      gate.allow?
      assert_equal 1, call_count
    end

    Timecop.freeze(now + 3.seconds) do
      gate.allow?
      assert_equal 1, call_count, "Should still be cached at 3s"
    end

    Timecop.freeze(now + 6.seconds) do
      gate.allow?
      assert_equal 2, call_count, "Should re-check after 6s (past 5s TTL)"
    end
  end
end


================================================
FILE: spec/mosquito/runnable_spec.cr
================================================
require "../spec_helper"

class Namespace::ConcreteRunnable
  include Mosquito::Runnable

  getter first_run_notifier = Channel(Bool).new
  getter first_run = true
  property state : Mosquito::Runnable::State

  # Testing wedge which calls: run, waits for a run to happen, and then calls stop.
  def test_run : Nil
    run
    first_run_notifier.receive
    stop.wait
  end

  def runnable_name : String
    "concrete_runnable"
  end

  def each_run : Nil
    if first_run
      @first_run = false
      first_run_notifier.send true
    end
    Fiber.yield
  end

  def stop(wait_group : WaitGroup = WaitGroup.new(1)) : WaitGroup
    first_run_notifier.close
    super(wait_group)
  end
end

describe Mosquito::Runnable do
  let(:runnable) { Namespace::ConcreteRunnable.new }

  it "builds a my_name" do
    assert_equal "namespace.concrete_runnable.#{runnable.object_id}", runnable.my_name
  end

  describe "run" do
    it "should log a startup message" do
      clear_logs
      runnable.test_run
      assert_logs_match "mosquito.concrete_runnable", "starting"
    end

    it "should log a finished message" do
      clear_logs
      runnable.test_run
      assert_logs_match "mosquito.concrete_runnable", "stopped"
    end
  end

  describe "stop" do
    it "should set the stopping flag" do
      runnable.state = Mosquito::Runnable::State::Working
      runnable.stop
      assert_equal Mosquito::Runnable::State::Stopping, runnable.state
    end

    it "should set the finished flag" do
      runnable.test_run
      assert_equal Mosquito::Runnable::State::Finished, runnable.state
    end
  end
end


================================================
FILE: spec/mosquito/runners/coordinator_spec.cr
================================================
require "../../spec_helper"

describe "Mosquito::Runners::Coordinator" do
  getter(queue : Queue) { test_job.class.queue }
  getter(test_job) { QueuedTestJob.new }
  getter(queue_list) { MockQueueList.new }
  getter(coordinator) { MockCoordinator.new queue_list }
  getter(enqueue_time) { Time.utc }

  def enqueue_job_run : JobRun
    queue_list.discovered_queues << queue

    job_run = JobRun.new "blah"

    Timecop.freeze enqueue_time do |t|
      job_run = test_job.enqueue in: 3.seconds
    end

    assert_includes queue.backend.list_scheduled, job_run.id
    job_run
  end

  def opt_in_to_locking
    Mosquito.temp_config(use_distributed_lock: true) do
      Mosquito.backend.delete Mosquito.backend.build_key(:coordinator, :leadership_lock)
      yield
      Mosquito.backend.delete Mosquito.backend.build_key(:coordinator, :leadership_lock)
    end
  end

  describe "only_if_coordinator" do
    getter(coordinator1) { Mosquito::Runners::Coordinator.new queue_list }
    getter(coordinator2) { Mosquito::Runners::Coordinator.new queue_list }

    it "gets a lock from the backend" do
      opt_in_to_locking do
        gotten = false

        coordinator1.only_if_coordinator do
          gotten = true
        end

        assert gotten
      end
    end

    it "fails to get a lock from the backend" do
      opt_in_to_locking do
        gotten = false

        coordinator1.only_if_coordinator do
          coordinator2.only_if_coordinator do
            gotten = true
          end
        end

        refute gotten
      end
    end

    it "releases the lock when release_leadership_lock is called" do
      opt_in_to_locking do
        gotten = false

        coordinator1.only_if_coordinator do
        end

        coordinator1.release_leadership_lock

        coordinator2.only_if_coordinator do
          gotten = true
        end

        assert gotten
      end
    end

    it "sets a ttl on the lock" do
      opt_in_to_locking do
        coordinator1.only_if_coordinator do
          assert Mosquito.backend.expires_in(coordinator.lock_key) > 0
        end
      end
    end

    it "retains leadership across calls" do
      opt_in_to_locking do
        count = 0

        3.times do
          coordinator1.only_if_coordinator do
            count += 1
          end
        end

        assert_equal 3, count
        assert coordinator1.is_leader?
      end
    end

    it "yields without locking when distributed lock is disabled" do
      Mosquito.temp_config(use_distributed_lock: false) do
        gotten = false

        coordinator1.only_if_coordinator do
          gotten = true
        end

        assert gotten
      end
    end
  end

  describe "enqueue_periodic_jobs" do
    it "enqueues a scheduled job_run at the appropriate time" do
      clean_slate do
        queue = PeriodicTestJob.queue
        Mosquito::Base.register_job_mapping PeriodicTestJob.name, PeriodicTestJob
        Mosquito::Base.register_job_interval PeriodicTestJob, interval: 1.second

        Timecop.freeze(enqueue_time) do
          coordinator.enqueue_periodic_jobs
        end

        queued_job_runs = queue.backend.list_waiting
        assert queued_job_runs.size >= 1

        last_job_run = queued_job_runs.last
        job_run_metadata = Mosquito.backend.retrieve JobRun.config_key(last_job_run)

        assert_equal enqueue_time.to_unix_ms.to_s, job_run_metadata["enqueue_time"]
      end
    end
  end

  describe "enqueue_delayed_jobs" do
    it "enqueues a delayed job_run when it's ready" do
      clean_slate do
        job_run = enqueue_job_run
        run_time = enqueue_time + 3.seconds

        Timecop.freeze run_time do |t|
          coordinator.enqueue_delayed_jobs
        end

        queued_job_runs = queue.backend.list_waiting
        assert_includes queued_job_runs, job_run.id

        last_job_run = queued_job_runs.last
        job_run_metadata = Mosquito.backend.retrieve JobRun.config_key(last_job_run)

        assert_equal queue.name, job_run_metadata["type"]?
      end
    end

    it "doesn't enqueue job_runs that arent ready yet" do
      clean_slate do
        job_run = enqueue_job_run

        check_time = enqueue_time + 2.999.seconds

        Timecop.freeze check_time do |t|
          coordinator.enqueue_delayed_jobs
        end

        queued_job_runs = queue.backend.list_waiting

        # does not deschedule and enqueue anything
        assert_equal 0, queued_job_runs.size
      end
    end

    it "logs when it finds delayed job_runs" do
      clean_slate do
        clear_logs
        enqueue_job_run
        Timecop.freeze enqueue_time + 3.seconds do |t|
          coordinator.enqueue_delayed_jobs
        end
        assert_logs_match "1 delayed jobs ready"
      end
    end

  end
end


================================================
FILE: spec/mosquito/runners/executor_spec.cr
================================================
require "../../spec_helper"

describe "Mosquito::Runners::Executor" do
  getter(queue_list) { MockQueueList.new }
  getter(overseer) { MockOverseer.new }
  getter(executor) { MockExecutor.new overseer.as(Mosquito::Runners::Overseer) }
  getter(api) { Mosquito::Api::Executor.new executor.object_id.to_s }
  getter(coordinator) { Mosquito::Runners::Coordinator.new queue_list }

  def register(job_class : Mosquito::Job.class)
    Mosquito::Base.register_job_mapping job_class.name.underscore, job_class
    queue_list.discovered_queues << job_class.queue
  end

  def run_job(job_class : Mosquito::Job.class)
    register job_class
    job_class.reset_performance_counter!
    job_run = job_class.new.enqueue
    executor.work_unit = WorkUnit.of(job_run, from: job_class.queue)
    executor.execute
  end

  describe "status" do
    it "starts as starting" do
      assert_equal Runnable::State::Starting, executor.state
    end

    it "broadcasts a ping when transitioning to idle" do
      executor.state = Runnable::State::Idle

      select
      when overseer.finished_notifier.receive
        assert true
      when timeout(0.5.seconds)
        refute true, "Timed out waiting for idle notifier"
      end
    end

    it "goes idle in pre_run" do
      executor.pre_run
      assert_equal Runnable::State::Idle, executor.state
    end
  end

  describe "running jobs" do
    it "runs a job from a queue" do
      clean_slate do
        run_job QueuedTestJob
        assert_equal 1, QueuedTestJob.performances
      end
    end

    it "reschedules a job that failed" do
      clean_slate do
        register FailingJob
        now = Time.utc

        job = FailingJob.new
        job_run = job.build_job_run
        job_run.store
        FailingJob.queue.enqueue job_run

        Timecop.freeze now do
          executor.work_unit = WorkUnit.of(job_run, from: FailingJob.queue)
          executor.execute
        end

        job_run.reload
        assert_equal 1, job_run.retry_count

        Timecop.freeze now + job.reschedule_interval(1) do
          coordinator.enqueue_delayed_jobs
          executor.work_unit = WorkUnit.of(job_run, from: FailingJob.queue)
          executor.execute
        end

        job_run.reload
        assert_equal 2, job_run.retry_count
      end
    end

    it "schedules deletion of a job_run that hard failed" do
      clean_slate do
        register NonReschedulableFailingJob

        job = NonReschedulableFailingJob.new
        job_run = job.build_job_run
        job_run.store
        NonReschedulableFailingJob.queue.enqueue job_run

        executor.work_unit = WorkUnit.of(job_run, from: NonReschedulableFailingJob.queue)
        executor.execute

        actual_ttl = backend.expires_in job_run.config_key
        assert_equal executor.failed_job_ttl, actual_ttl
      end
    end

    it "purges a successful job_run from the backend" do
      clean_slate do
        register QueuedTestJob

        job = QueuedTestJob.new
        job_run = job.build_job_run
        job_run.store
        QueuedTestJob.queue.enqueue job_run

        executor.work_unit = WorkUnit.of(job_run, from: QueuedTestJob.queue)
        executor.execute

        assert_logs_match "Success"

        QueuedTestJob.queue.enqueue job_run
        actual_ttl = Mosquito.backend.expires_in job_run.config_key
        assert_equal executor.successful_job_ttl, actual_ttl
      end
    end

    it "doesnt reschedule a job that cant be rescheduled" do
      clean_slate do
        run_job NonReschedulableFailingJob
        assert_logs_match "cannot be rescheduled"
      end
    end

    it "tells the observer what it's working on" do
      SleepyJob.should_sleep = true
      job = SleepyJob.new
      job_run = job.build_job_run
      job_run.store

      job_started = Channel(Bool).new
      job_finished = Channel(Bool).new

      # Eagerly evaluate to avoid race condition in lazy
      # getter initialization across fibers.
      executor
      api

      spawn {
        executor.work_unit = WorkUnit.of(job_run, from: SleepyJob.queue)
        executor.execute
        job_finished.send true
      }

      spawn {
        loop {
          break if api.current_job
        }
        assert_equal job_run.id, api.current_job
        assert_equal SleepyJob.queue.name, api.current_job_queue
        job_started.send true
      }

      select
      when job_started.receive
      when timeout(0.5.seconds)
        refute true, "Timed out waiting for job to start"
      end

      SleepyJob.should_sleep = false

      select
      when job_finished.receive
      when timeout(0.5.seconds)
        refute true, "Timed out waiting for job to finish"
      end

      assert_nil api.current_job, "Job should be cleared after finishing"
      assert_nil api.current_job_queue, "Queue should be cleared after finishing"
    end
  end

  describe "logs success/failures messages" do
    it "logs a success message when the job succeeds" do
      clean_slate do
        run_job QueuedTestJob
        assert_logs_match "Success"
      end
    end

    it "logs a failure message when the job fails" do
      clean_slate do
        run_job FailingJob
        assert_logs_match "Failure"
      end
    end
  end

  describe "job timing messages" do
    it "logs the time a job took to run" do
      clean_slate do
        run_job QueuedTestJob
        assert_logs_match "and took"
      end
    end

    it "logs the time a job took to run when the job fails" do
      clean_slate do
        run_job FailingJob
        assert_logs_match "taking"
      end
    end
  end

  describe "start and finish messages" do
    it "logs the job run start message" do
      clean_slate do
        run_job QueuedTestJob
        assert_logs_match "Starting: queued_test_job"
      end
    end
  end
end


================================================
FILE: spec/mosquito/runners/overseer_spec.cr
================================================
require "../../spec_helper"

describe "Mosquito::Runners::Overseer" do
  getter(overseer : MockOverseer) { MockOverseer.new }
  getter(queue_list : MockQueueList ) { overseer.queue_list.as(MockQueueList) }
  getter(coordinator : MockCoordinator ) { overseer.coordinator.as(MockCoordinator) }
  getter(executor : MockExecutor) { overseer.executors.first.as(MockExecutor) }

  def register(job_class : Mosquito::Job.class)
    Mosquito::Base.register_job_mapping job_class.name.underscore, job_class
    queue_list.discovered_queues << job_class.queue
  end

  def run_job(job_class : Mosquito::Job.class)
    register job_class
    job_class.reset_performance_counter!
    job_run = job_class.new.enqueue
    executor.execute job_run, from_queue: job_class.queue
  end

  describe "pre_run" do
    it "runs all executors" do
      overseer.executors.each do |executor|
        assert_equal Runnable::State::Starting, executor.state
      end
      overseer.pre_run
      overseer.executors.each do |executor|
        assert_equal Runnable::State::Working, executor.state
      end
    end
  end

  describe "post_run" do
    it "stops all executors" do
      overseer.executors.each(&.run)
      overseer.post_run
      overseer.executors.each do |executor|
        assert_equal Runnable::State::Finished, executor.state
      end
    end

    it "logs messages about stopping the executors" do
      clear_logs
      overseer.pre_run
      overseer.post_run
      assert_logs_match "Stopping executors."
      assert_logs_match "All executors stopped."
    end
  end

  describe "each_run" do
    it "dequeues a job and dispatches it to the pipeline" do
      clean_slate do
        register QueuedTestJob
        expected_job_run = QueuedTestJob.new.enqueue

        overseer.work_handout = Channel(WorkUnit).new

        queue_list.state = Runnable::State::Working
        executor.state = Runnable::State::Idle

        # each_run will block until there's a receiver on the channel
        spawn { overseer.each_run }
        result = overseer.work_handout.receive
        assert_equal expected_job_run, result.job_run
        assert_equal QueuedTestJob.queue, result.queue
      end
    end

    it "waits #idle_wait before checking the queue again" do
      clean_slate do
        # an idle executor, but no jobs in the queue
        executor.state = Runnable::State::Idle
        queue_list.state = Runnable::State::Working

        tick_time = Time.measure do
          overseer.each_run
        end

        assert tick_time >= overseer.idle_wait, "Expected to wait at least #{overseer.idle_wait}, but only waited #{tick_time}"
      end
    end

    it "triggers the scheduler" do
      assert_equal 0, coordinator.schedule_count
      overseer.each_run
      assert_equal 1, coordinator.schedule_count
    end
  end

  describe "dequeue_job? stamps overseer_id" do
    it "claims the job run with the overseer's instance id on dequeue" do
      clean_slate do
        register QueuedTestJob
        job_run = QueuedTestJob.new.enqueue

        queue_list.state = Runnable::State::Working

        result = overseer.dequeue_job?
        assert result
        assert_equal overseer.observer.instance_id, result.not_nil!.job_run.overseer_id
      end
    end
  end

  describe "remote executor count" do
    it "applies the remote executor count on each_run" do
      clean_slate do
        Mosquito.configuration.overseer_id = "test-worker"
        Mosquito::Api.set_executor_count(3, overseer_id: "test-worker")

        queue_list.state = Runnable::State::Working
        overseer.each_run

        assert_equal 3, overseer.executor_count
      ensure
        Mosquito.configuration.overseer_id = nil
      end
    end

    it "prefers per-overseer count over global" do
      clean_slate do
        Mosquito.configuration.overseer_id = "test-worker"
        Mosquito::Api.set_executor_count(10)
        Mosquito::Api.set_executor_count(2, overseer_id: "test-worker")

        queue_list.state = Runnable::State::Working
        overseer.each_run

        assert_equal 2, overseer.executor_count
      ensure
        Mosquito.configuration.overseer_id = nil
      end
    end

    it "falls back to global when no per-overseer count is set" do
      clean_slate do
        Mosquito.configuration.overseer_id = "test-worker"
        Mosquito::Api.set_executor_count(7)

        queue_list.state = Runnable::State::Working
        overseer.each_run

        assert_equal 7, overseer.executor_count
      ensure
        Mosquito.configuration.overseer_id = nil
      end
    end

    it "does not change executor_count when no remote value is set" do
      clean_slate do
        original_count = overseer.executor_count

        queue_list.state = Runnable::State::Working
        overseer.each_run

        assert_equal original_count, overseer.executor_count
      end
    end

    it "clamps an invalid remote executor count of 0 to 1" do
      clean_slate do
        Mosquito.configuration.overseer_id = "test-worker"
        Mosquito::Api.set_executor_count(0, overseer_id: "test-worker")

        queue_list.state = Runnable::State::Working
        overseer.each_run

        assert_equal 1, overseer.executor_count
      ensure
        Mosquito.configuration.overseer_id = nil
      end
    end
  end

  describe "cleanup_orphaned_pending_jobs" do
    it "recovers a pending job whose overseer is dead" do
      clean_slate do
        register QueuedTestJob

        # Use a separate overseer that won't be registered as alive.
        dead_overseer = MockOverseer.new

        job = QueuedTestJob.new
        job_run = job.build_job_run
        job_run.store
        QueuedTestJob.queue.enqueue job_run
        QueuedTestJob.queue.dequeue
        job_run.claimed_by dead_overseer

        # Verify job is stuck in pending
        assert_includes QueuedTestJob.queue.backend.list_pending, job_run.id
        assert_equal 0, job_run.retry_count

        # Register only the *live* overseer
        Mosquito.backend.register_overseer overseer.observer.instance_id

        # Run cleanup — dead_overseer's id won't be in the active set
        overseer.cleanup_orphaned_pending_jobs

        # Job should be removed from pending and rescheduled
        assert_empty QueuedTestJob.queue.backend.list_pending
        assert_includes QueuedTestJob.queue.backend.list_scheduled, job_run.id

        # Retry count should be incremented
        job_run.reload
        assert_equal 1, job_run.retry_count
      end
    end

    it "does not touch pending jobs from a live overseer" do
      clean_slate do
        register QueuedTestJob

        job = QueuedTestJob.new
        job_run = job.build_job_run
        job_run.store
        QueuedTestJob.queue.enqueue job_run
        QueuedTestJob.queue.dequeue

        # Claim with the live overseer
        Mosquito.backend.register_overseer overseer.observer.instance_id
        job_run.claimed_by overseer

        assert_includes QueuedTestJob.queue.backend.list_pending, job_run.id

        overseer.cleanup_orphaned_pending_jobs

        # Job should still be in pending — its overseer is alive
        assert_includes QueuedTestJob.queue.backend.list_pending, job_run.id
      end
    end

    it "claims unclaimed pending jobs without recovering them" do
      clean_slate do
        register QueuedTestJob

        job = QueuedTestJob.new
        job_run = job.build_job_run
        job_run.store
        QueuedTestJob.queue.enqueue job_run
        QueuedTestJob.queue.dequeue

        # No claim — simulates a job from before this feature
        assert_nil job_run.overseer_id
        assert_includes QueuedTestJob.queue.backend.list_pending, job_run.id

        Mosquito.backend.register_overseer overseer.observer.instance_id
        overseer.cleanup_orphaned_pending_jobs

        # Job should still be in pending (not recovered)
        assert_includes QueuedTestJob.queue.backend.list_pending, job_run.id

        # But it should now be claimed by this overseer
        job_run.reload
        assert_equal overseer.observer.instance_id, job_run.overseer_id
      end
    end

    it "banishes an orphaned job that has exhausted retries" do
      clean_slate do
        register QueuedTestJob

        dead_overseer = MockOverseer.new

        # Create a job_run with retry_count=4 so the next failure (count=5)
        # exceeds the default rescheduleable? limit of < 5.
        job_run = Mosquito::JobRun.new("queued_test_job", retry_count: 4)
        job_run.store

        QueuedTestJob.queue.enqueue job_run
        QueuedTestJob.queue.dequeue
        job_run.claimed_by dead_overseer

        assert_includes QueuedTestJob.queue.backend.list_pending, job_run.id

        Mosquito.backend.register_overseer overseer.observer.instance_id
        overseer.cleanup_orphaned_pending_jobs

        # Job should be removed from pending and moved to dead
        assert_empty QueuedTestJob.queue.backend.list_pending
        assert_empty QueuedTestJob.queue.backend.list_waiting
        assert_empty QueuedTestJob.queue.backend.list_scheduled
        assert_includes QueuedTestJob.queue.backend.list_dead, job_run.id
      end
    end
  end
end


================================================
FILE: spec/mosquito/runners/queue_list_spec.cr
================================================
require "../../spec_helper"

describe "Mosquito::Runners::QueueList" do
  getter(queue_list) { MockQueueList.new }

  def enqueue_jobs
    PassingJob.new.enqueue
    FailingJob.new.enqueue
    EchoJob.new(text: "hello world").enqueue
  end

  describe "each_run" do
    it "returns a list of queues" do
      clean_slate do
        enqueue_jobs
        queue_list.each_run
        assert_equal ["failing_job", "io_queue", "passing_job"], queue_list.queues.map(&.name).sort
      end
    end

    it "logs a message about the number of fetched queues" do
      clean_slate do
        clear_logs
        enqueue_jobs
        queue_list.each_run
        assert_logs_match "found 3 new queues"
      end
    end
  end

  describe "queue filtering" do
    it "filters the list of queues when a whitelist is present" do
      clean_slate do
        enqueue_jobs

        Mosquito.temp_config(run_from: ["io_queue", "passing_job"]) do
          queue_list.each_run
        end
      end

      assert_equal ["io_queue", "passing_job"], queue_list.queues.map(&.name).sort
    end

    it "logs an error when all queues are filtered out" do
      clean_slate do
        enqueue_jobs

        Mosquito.temp_config(run_from: ["test4"]) do
          queue_list.each_run
        end

        assert_logs_match "No watchable queues found."
      end
    end

    it "doesnt log an error when no queues are present" do
      clean_slate do
        queue_list.each_run
        refute_logs_match "No watchable queues found."
      end
    end
  end

  describe "paused queue filtering" do
    it "excludes paused queues from the queue list" do
      clean_slate do
        enqueue_jobs
        Mosquito::Queue.new("passing_job").pause
        queue_list.each_run
        assert_equal ["failing_job", "io_queue"], queue_list.queues.map(&.name).sort
      end
    end

    it "logs a message about paused queues" do
      clean_slate do
        clear_logs
        enqueue_jobs
        Mosquito::Queue.new("passing_job").pause
        queue_list.each_run
        assert_logs_match "1 paused queues: passing_job"
      end
    end

    it "includes queues again after they are resumed" do
      clean_slate do
        enqueue_jobs
        q = Mosquito::Queue.new("passing_job")
        q.pause
        queue_list.each_run
        refute_includes queue_list.queues.map(&.name), "passing_job"

        q.resume
        queue_list.each_run
        assert_includes queue_list.queues.map(&.name), "passing_job"
      end
    end
  end

  describe "resource gate filtering" do
    it "excludes queues whose gate blocks" do
      clean_slate do
        enqueue_jobs
        queue_list.each_run

        gate = Mosquito::ThresholdGate.new(threshold: 80.0, sample_ttl: 0.seconds) { 90.0 }
        queue_list.resource_gates = {"passing_job" => gate.as(Mosquito::ResourceGate)}

        refute_includes queue_list.queues.map(&.name), "passing_job"
        assert_includes queue_list.queues.map(&.name), "failing_job"
        assert_includes queue_list.queues.map(&.name), "io_queue"
      end
    end

    it "includes queues whose gate allows" do
      clean_slate do
        enqueue_jobs
        queue_list.each_run

        gate = Mosquito::ThresholdGate.new(threshold: 80.0, sample_ttl: 0.seconds) { 50.0 }
        queue_list.resource_gates = {"passing_job" => gate.as(Mosquito::ResourceGate)}

        assert_includes queue_list.queues.map(&.name), "passing_job"
      end
    end

    it "ungated queues are always included" do
      clean_slate do
        enqueue_jobs
        queue_list.each_run

        gate = Mosquito::ThresholdGate.new(threshold: 80.0, sample_ttl: 0.seconds) { 90.0 }
        queue_list.resource_gates = {"passing_job" => gate.as(Mosquito::ResourceGate)}

        assert_equal 2, queue_list.queues.size
      end
    end

    it "multiple queues can share a gate" do
      clean_slate do
        enqueue_jobs
        queue_list.each_run

        gate = Mosquito::ThresholdGate.new(threshold: 80.0, sample_ttl: 0.seconds) { 90.0 }
        queue_list.resource_gates = {
          "passing_job" => gate.as(Mosquito::ResourceGate),
          "failing_job" => gate.as(Mosquito::ResourceGate),
        }

        assert_equal ["io_queue"], queue_list.queues.map(&.name)
      end
    end

    it "gate state is evaluated on each access" do
      clean_slate do
        enqueue_jobs
        queue_list.each_run

        value = 90.0
        gate = Mosquito::ThresholdGate.new(threshold: 80.0, sample_ttl: 0.seconds) { value }
        queue_list.resource_gates = {"passing_job" => gate.as(Mosquito::ResourceGate)}

        refute_includes queue_list.queues.map(&.name), "passing_job"

        value = 50.0
        assert_includes queue_list.queues.map(&.name), "passing_job"
      end
    end

    it "returns all queues when no gates are configured" do
      clean_slate do
        enqueue_jobs
        queue_list.each_run

        assert_equal 3, queue_list.queues.size
      end
    end
  end
end


================================================
FILE: spec/mosquito/runners/run_at_most_spec.cr
================================================
require "../../spec_helper"

class RunsAtMostMock
  include Mosquito::Runners::RunAtMost

  def yield_once_a_second(&block)
    run_at_most every: 1.second, label: :testing do |t|
      yield
    end
  end
end

describe "Mosquito::yielder#run_at_most" do
  getter(yielder) { RunsAtMostMock.new }

  it "prevents throttled blocks from running too often" do
    count = 0

    2.times do
      yielder.yield_once_a_second do
        count += 1
      end
    end

    assert_equal 1, count
  end

  it "allows throttled blocks to run only after enough time has passed" do
    count = 0
    moment = Time.utc
    yielder
    incrementy = ->() do
      yielder.yield_once_a_second do
        count += 1
      end
    end

    # Should increment
    Timecop.freeze moment do |time|
      incrementy.call
    end

    # Should not increment
    # Move ahead 0.999 seconds
    Timecop.freeze(moment + 999.milliseconds) do |time|
      incrementy.call
    end

    assert_equal 1, count

    # Should increment
    # Move ahead the rest of the second
    moment += 1.1.seconds
    Timecop.freeze(moment) do |time|
      incrementy.call
    end

    assert_equal 2, count

    # Should not increment
    # Try again and it shouldn't increment
    Timecop.freeze(moment) do |time|
      incrementy.call
    end

    assert_equal 2, count
  end
end


================================================
FILE: spec/mosquito/serializers/primitive_serializers_spec.cr
================================================
require "uuid"
require "../../spec_helper"

class PrimitiveSerializerTester
  extend Mosquito::Serializers::Primitives
end

describe Mosquito::Serializers::Primitives do
  it "serializes uuids" do
    uuid = UUID.random
    assert_equal uuid, UUID.new(PrimitiveSerializerTester.serialize_uuid(uuid))
  end

  it "deserializes uuids" do
    uuid = UUID.random.to_s
    assert_equal uuid, PrimitiveSerializerTester.deserialize_uuid(uuid).to_s
  end
end


================================================
FILE: spec/mosquito/testing_backend_spec.cr
================================================
require "../spec_helper"

describe Mosquito::TestBackend do
  def latest_enqueued_job
    Mosquito::TestBackend.enqueued_jobs.last
  end

  it "holds a copy of jobs which have been enqueued" do
    Mosquito.temp_config(backend: Mosquito::TestBackend.new) do
      QueuedTestJob.new.enqueue
      assert_equal QueuedTestJob, latest_enqueued_job.klass
    end
  end

  it "embeds job parameters" do
    Mosquito.temp_config(backend: Mosquito::TestBackend.new) do
      EchoJob.new(text: "hello world").enqueue
      assert_equal "hello world", latest_enqueued_job.config["text"]
    end
  end

  it "hold the job id" do
    Mosquito.temp_config(backend: Mosquito::TestBackend.new) do
      job_run = QueuedTestJob.new.enqueue
      assert_equal job_run.id, latest_enqueued_job.id
    end
  end

  it "has a list of job runs which can be emptied" do
    Mosquito.temp_config(backend: Mosquito::TestBackend.new) do
      Mosquito::TestBackend.flush_enqueued_jobs!
      job_run = EchoJob.new(text: "hello world").enqueue
      assert_equal job_run.id, latest_enqueued_job.id
      Mosquito::TestBackend.flush_enqueued_jobs!
      assert Mosquito::TestBackend.enqueued_jobs.empty?
    end
  end
end


================================================
FILE: spec/mosquito/unique_job_spec.cr
================================================
require "../spec_helper"

describe Mosquito::UniqueJob do
  describe "first enqueue" do
    it "enqueues a job when no duplicate exists" do
      clean_slate do
        job = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job_run = job.enqueue
        enqueued = UniqueTestJob.queue.backend.list_waiting
        assert_equal [job_run.id], enqueued
      end
    end
  end

  describe "duplicate suppression" do
    it "prevents a second enqueue with the same parameters" do
      clean_slate do
        job1 = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job_run1 = job1.enqueue
        enqueued = UniqueTestJob.queue.backend.list_waiting
        assert_equal 1, enqueued.size

        job2 = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job_run2 = job2.enqueue
        enqueued = UniqueTestJob.queue.backend.list_waiting
        assert_equal 1, enqueued.size
      end
    end

    it "allows enqueue with different parameters" do
      clean_slate do
        job1 = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job1.enqueue
        enqueued = UniqueTestJob.queue.backend.list_waiting
        assert_equal 1, enqueued.size

        job2 = UniqueTestJob.new(user_id: 2_i64, email_type: "welcome")
        job2.enqueue
        enqueued = UniqueTestJob.queue.backend.list_waiting
        assert_equal 2, enqueued.size
      end
    end

    it "allows enqueue with different parameter values" do
      clean_slate do
        job1 = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job1.enqueue
        enqueued = UniqueTestJob.queue.backend.list_waiting
        assert_equal 1, enqueued.size

        job2 = UniqueTestJob.new(user_id: 1_i64, email_type: "reminder")
        job2.enqueue
        enqueued = UniqueTestJob.queue.backend.list_waiting
        assert_equal 2, enqueued.size
      end
    end
  end

  describe "key filtering" do
    it "considers only specified key fields for uniqueness" do
      clean_slate do
        # Same user_id, different message — should be suppressed because
        # key is only [:user_id]
        job1 = UniqueWithKeyJob.new(user_id: 1_i64, message: "hello")
        job1.enqueue
        enqueued = UniqueWithKeyJob.queue.backend.list_waiting
        assert_equal 1, enqueued.size

        job2 = UniqueWithKeyJob.new(user_id: 1_i64, message: "world")
        job2.enqueue
        enqueued = UniqueWithKeyJob.queue.backend.list_waiting
        assert_equal 1, enqueued.size
      end
    end

    it "allows enqueue when key fields differ" do
      clean_slate do
        job1 = UniqueWithKeyJob.new(user_id: 1_i64, message: "hello")
        job1.enqueue
        enqueued = UniqueWithKeyJob.queue.backend.list_waiting
        assert_equal 1, enqueued.size

        job2 = UniqueWithKeyJob.new(user_id: 2_i64, message: "hello")
        job2.enqueue
        enqueued = UniqueWithKeyJob.queue.backend.list_waiting
        assert_equal 2, enqueued.size
      end
    end
  end

  describe "expiration" do
    it "allows re-enqueue after the uniqueness window expires" do
      clean_slate do
        job1 = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job_run1 = job1.enqueue
        enqueued = UniqueTestJob.queue.backend.list_waiting
        assert_equal 1, enqueued.size

        # Manually remove the lock to simulate expiration
        lock_key = job1.uniqueness_key(job_run1)
        Mosquito.backend.unlock(lock_key, job_run1.id)

        job2 = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job2.enqueue
        enqueued = UniqueTestJob.queue.backend.list_waiting
        assert_equal 2, enqueued.size
      end
    end
  end

  describe "no parameters" do
    it "works with jobs that have no parameters" do
      clean_slate do
        job1 = UniqueNoParamsJob.new
        job1.enqueue
        enqueued = UniqueNoParamsJob.queue.backend.list_waiting
        assert_equal 1, enqueued.size

        job2 = UniqueNoParamsJob.new
        job2.enqueue
        enqueued = UniqueNoParamsJob.queue.backend.list_waiting
        assert_equal 1, enqueued.size
      end
    end
  end

  describe "delayed enqueue" do
    it "prevents duplicate delayed enqueue" do
      clean_slate do
        job1 = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job1.enqueue(in: 5.minutes)
        scheduled = UniqueTestJob.queue.backend.list_scheduled
        assert_equal 1, scheduled.size

        job2 = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job2.enqueue(in: 10.minutes)
        scheduled = UniqueTestJob.queue.backend.list_scheduled
        assert_equal 1, scheduled.size
      end
    end

    it "prevents duplicate when mixing immediate and delayed enqueue" do
      clean_slate do
        job1 = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job1.enqueue
        waiting = UniqueTestJob.queue.backend.list_waiting
        assert_equal 1, waiting.size

        job2 = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
        job2.enqueue(in: 5.minutes)
        scheduled = UniqueTestJob.queue.backend.list_scheduled
        assert_equal 0, scheduled.size
      end
    end
  end

  describe "unique_duration" do
    it "returns the configured duration" do
      job = UniqueTestJob.new(user_id: 1_i64, email_type: "welcome")
      assert_equal 1.hour, job.unique_duration
    end
  end
end


================================================
FILE: spec/mosquito/version_spec.cr
================================================
require "../spec_helper"
require "yaml"

describe "mosquito version numbers" do
  it "is defined" do
    assert Mosquito::VERSION
  end

  it "matches the shard.yml file" do
    File.open("shard.yml") do |file|
      assert_equal Mosquito::VERSION, YAML.parse(file)["version"].as_s
    end
  end
end


================================================
FILE: spec/spec_helper.cr
================================================
require "minitest"
require "minitest/focus"

require "log"
Log.setup :fatal

require "timecop"
Timecop.safe_mode = true

require "../src/mosquito"
Mosquito.configure do |settings|
  settings.backend_connection_string = testing_redis_url
  settings.publish_metrics = true
end

require "./helpers/*"
class Minitest::Test
  include PubSub::Helpers
end

Mosquito.configuration.backend.flush

require "minitest/autorun"


================================================
FILE: src/mosquito/api/concurrency_config.cr
================================================
module Mosquito
  # Provides read/write access to the remotely stored concurrency limits
  # used by `RemoteConfigDequeueAdapter`.
  #
  # Supports both global limits (shared by all overseers) and per-overseer
  # limits for asymmetric hardware configurations.
  #
  # ```crystal
  # config = Mosquito::Api::ConcurrencyConfig.instance
  # config.limits                                  # => global limits
  # config.limits(overseer_id: "gpu-worker-1")     # => per-overseer limits
  # config.update({"queue_a" => 5})                # write global
  # config.update({"queue_a" => 1}, overseer_id: "gpu-worker-1")  # write per-overseer
  # config.clear                                   # remove global limits
  # config.clear(overseer_id: "gpu-worker-1")      # remove per-overseer limits
  # ```
  class Api::ConcurrencyConfig
    def self.instance : self
      new
    end

    # Returns the global concurrency limits stored in the backend.
    def limits : Hash(String, Int32)
      RemoteConfigDequeueAdapter.stored_limits
    end

    # Returns the concurrency limits stored for a specific overseer.
    def limits(overseer_id : String) : Hash(String, Int32)
      RemoteConfigDequeueAdapter.stored_limits(overseer_id)
    end

    # Overwrites the global stored concurrency limits with *new_limits*.
    def update(new_limits : Hash(String, Int32)) : Nil
      RemoteConfigDequeueAdapter.store_limits(new_limits)
    end

    # Overwrites the stored concurrency limits for a specific overseer.
    def update(new_limits : Hash(String, Int32), overseer_id : String) : Nil
      RemoteConfigDequeueAdapter.store_limits(new_limits, overseer_id)
    end

    # Removes all globally stored concurrency limits.
    def clear : Nil
      RemoteConfigDequeueAdapter.clear_limits
    end

    # Removes stored concurrency limits for a specific overseer.
    def clear(overseer_id : String) : Nil
      RemoteConfigDequeueAdapter.clear_limits(overseer_id)
    end
  end
end


================================================
FILE: src/mosquito/api/executor.cr
================================================
module Mosquito
  module Api
    # An interface for an executor.
    #
    # This is used to inspect the state of an executor. For more information about executors, see `Mosquito::Runners::Executor`.
    class Executor
      getter :instance_id
      private getter :metadata

      # Creates an executor inspector.
      # The metadata is readonly and can be used to inspect the state of the executor.
      #
      # see #current_job, #current_job_queue
      def initialize(@instance_id : String)
        @metadata = Metadata.new Observability::Executor.metadata_key(@instance_id), readonly: true
      end

      # The current job being executed by the executor.
      #
      # When the executor is idle, this will be `nil`.
      def current_job : String?
        metadata["current_job"]?
      end

      # The queue which housed the current job being executed.
      #
      # When the executor is idle, this will be `nil`.
      def current_job_queue : String?
        metadata["current_job_queue"]?
      end

      # The last heartbeat time, or nil if none exists.
      def heartbeat : Time?
        metadata.heartbeat?
      end
    end
  end

  module Observability
    class Executor
      include Publisher

      private getter log : ::Log
      def self.metadata_key(instance_id : String) : String
        Mosquito.backend.build_key "executor", instance_id
      end

      def initialize(executor : Mosquito::Runners::Executor)
        @metadata = Metadata.new self.class.metadata_key executor.object_id.to_s
        @log = Log.for(executor.runnable_name)
        overseer_publish_context = executor.overseer.observer.publish_context
        @publish_context = PublishContext.new(
          overseer_publish_context,
          [:executor, executor.object_id]
        )
      end

      def execute(job_run : JobRun, from_queue : Mosquito::Queue)
        metrics do
          @metadata.set({
            "current_job" => job_run.id,
            "current_job_queue" => from_queue.name
          })
        end

        # Calculate what the duration _might_ be
        expected_duration = Mosquito.backend.average average_key(job_run.type)

        log.info { "#{"Starting:".colorize.magenta} #{job_run} from #{from_queue.name}" }
        publish({
          event: "job-started",
          job_run: job_run.id,
          from_queue: from_queue.name,
          expected_duration_ms: expected_duration
        })

        duration = Time.measure do
          yield
        end

        if job_run.succeeded?
          log_success_message job_run, duration
        elsif job_run.preempted?
          log_preempted_message job_run, duration
        else
          log_failure_message job_run, duration
        end

        publish({event: "job-finished", job_run: job_run.id})

        metrics do
          key = average_key(job_run.type)
          Mosquito.backend.average_push key, duration.total_milliseconds.to_i
          Mosquito.backend.delete key, in: 30.days

          @metadata.set(
            current_job: nil,
            current_job_queue: nil
          )
        end
      end

      def average_key(job_run_type : String) : String
        Mosquito.backend.build_key "job", job_run_type, "duration"
      end

      def log_success_message(job_run : JobRun, duration : Time::Span)
        log.info { "#{"Success:".colorize.green} #{job_run} finished and took #{time_with_units duration}" }
      end

      def log_preempted_message(job_run : JobRun, duration : Time::Span)
        message = String::Builder.new
        message << "Preempted: ".colorize.cyan
        message << job_run
        message << " was preempted"

        reason = job_run.preempt_reason
        unless reason.empty?
          message << " ("
          message << reason
          message << ")"
        end

        message << " after "
        message << time_with_units duration

        if job_run.rescheduleable?
          next_execution = Time.utc + job_run.reschedule_interval
          message << " and will run again".colorize.cyan
          message << " in "
          message << job_run.reschedule_interval
          message << " (at "
          message << next_execution
          message << ")"
        end

        log.info { message.to_s }
      end

      def log_failure_message(job_run : JobRun, duration : Time::Span)
        message = String::Builder.new
        message << "Failure: ".colorize.red
        message << job_run
        message << " failed, taking "
        message << time_with_units duration
        message << " and "

        if job_run.rescheduleable?
          next_execution = Time.utc + job_run.reschedule_interval
          message << "will run again".colorize.cyan
          message << " in "
          message << job_run.reschedule_interval
          message << " (at "
          message << next_execution
          message << ")"
          log.warn { message.to_s }
        else
          message << "cannot be rescheduled".colorize.yellow
          log.error { message.to_s }
        end
      end

      # :nodoc:
      private def time_with_units(duration : Time::Span)
        seconds = duration.total_seconds
        if seconds > 0.1
          "#{(seconds).*(100).trunc./(100)}s".colorize.red
        elsif seconds > 0.001
          "#{(seconds * 1_000).trunc}ms".colorize.yellow
        elsif seconds > 0.000_001
          "#{(seconds * 1_000_000).trunc}µs".colorize.green
        elsif seconds > 0.000_000_001
          "#{(seconds * 1_000_000_000).trunc}ns".colorize.green
        else
          "no discernible time at all".colorize.green
        end
      end

      def heartbeat!
        metrics do
          @metadata.heartbeat!
        end
      end
    end
  end
end


================================================
FILE: src/mosquito/api/executor_config.cr
================================================
module Mosquito
  # Provides read/write access to the remotely stored executor count
  # used by overseers configured with a stable `overseer_id`.
  #
  # Supports both global counts (shared by all overseers) and per-overseer
  # counts for asymmetric hardware configurations.
  #
  # ```crystal
  # config = Mosquito::Api::ExecutorConfig.instance
  # config.executor_count                                  # => global count or nil
  # config.executor_count(overseer_id: "gpu-worker-1")     # => per-overseer count or nil
  # config.update(8)                                       # write global
  # config.update(2, overseer_id: "gpu-worker-1")          # write per-overseer
  # config.clear                                           # remove global o
Download .txt
gitextract_f3v7pvaw/

├── .claude/
│   ├── hooks/
│   │   └── session-start.sh
│   ├── settings.json
│   └── todo.md
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   └── bug.md
│   └── workflows/
│       ├── ci.yml
│       └── docs.yml
├── .gitignore
├── .tool-versions
├── :w
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── benchmark/
│   ├── benchmark.cr
│   └── jobs/
│       └── emit_message_job.cr
├── demo/
│   ├── jobs/
│   │   ├── custom_serializers.cr
│   │   ├── periodically_puts.cr
│   │   ├── queued_job.cr
│   │   ├── rate_limited_job.cr
│   │   └── unique_job.cr
│   └── run.cr
├── scripts/
│   ├── increment_version
│   ├── lib/
│   │   └── increment_version.sh
│   └── version_tag
├── shard.yml
├── spec/
│   ├── helpers/
│   │   ├── bare_base_class.cr
│   │   ├── configuration_helper.cr
│   │   ├── global_helpers.cr
│   │   ├── logging_helper.cr
│   │   ├── mock_coordinator.cr
│   │   ├── mock_executor.cr
│   │   ├── mock_overseer.cr
│   │   ├── mock_queue_list.cr
│   │   ├── mocks.cr
│   │   ├── null_dequeue_adapter.cr
│   │   ├── pub_sub.cr
│   │   └── spy_dequeue_adapter.cr
│   ├── mosquito/
│   │   ├── api/
│   │   │   ├── executor_config_spec.cr
│   │   │   ├── executor_spec.cr
│   │   │   ├── job_run_spec.cr
│   │   │   ├── overseer_spec.cr
│   │   │   ├── periodic_job_spec.cr
│   │   │   ├── publisher_spec.cr
│   │   │   └── queue_spec.cr
│   │   ├── api_spec.cr
│   │   ├── backend/
│   │   │   ├── deleting_spec.cr
│   │   │   ├── executor_spec.cr
│   │   │   ├── expiring_list_spec.cr
│   │   │   ├── hash_storage_spec.cr
│   │   │   ├── inspection_spec.cr
│   │   │   ├── lock_spec.cr
│   │   │   ├── overseer_spec.cr
│   │   │   └── queueing_spec.cr
│   │   ├── backend_spec.cr
│   │   ├── base_spec.cr
│   │   ├── configuration_spec.cr
│   │   ├── dequeue_adapters/
│   │   │   ├── concurrency_limited_dequeue_adapter_spec.cr
│   │   │   ├── remote_config_dequeue_adapter_spec.cr
│   │   │   ├── shuffle_dequeue_adapter_spec.cr
│   │   │   └── weighted_dequeue_adapter_spec.cr
│   │   ├── exceptions_spec.cr
│   │   ├── job/
│   │   │   └── job_state_spec.cr
│   │   ├── job_run/
│   │   │   ├── rescheduling_spec.cr
│   │   │   ├── running_spec.cr
│   │   │   └── storage_spec.cr
│   │   ├── job_run_spec.cr
│   │   ├── job_spec.cr
│   │   ├── key_builder_spec.cr
│   │   ├── metadata_spec.cr
│   │   ├── periodic_job_run_spec.cr
│   │   ├── periodic_job_spec.cr
│   │   ├── queue_spec.cr
│   │   ├── queued_job_spec.cr
│   │   ├── rate_limiter_spec.cr
│   │   ├── resource_gate_spec.cr
│   │   ├── runnable_spec.cr
│   │   ├── runners/
│   │   │   ├── coordinator_spec.cr
│   │   │   ├── executor_spec.cr
│   │   │   ├── overseer_spec.cr
│   │   │   ├── queue_list_spec.cr
│   │   │   └── run_at_most_spec.cr
│   │   ├── serializers/
│   │   │   └── primitive_serializers_spec.cr
│   │   ├── testing_backend_spec.cr
│   │   ├── unique_job_spec.cr
│   │   └── version_spec.cr
│   └── spec_helper.cr
└── src/
    ├── mosquito/
    │   ├── api/
    │   │   ├── concurrency_config.cr
    │   │   ├── executor.cr
    │   │   ├── executor_config.cr
    │   │   ├── job_run.cr
    │   │   ├── observability/
    │   │   │   └── publisher.cr
    │   │   ├── overseer.cr
    │   │   ├── periodic_job.cr
    │   │   ├── queue.cr
    │   │   └── queue_list.cr
    │   ├── api.cr
    │   ├── backend.cr
    │   ├── base.cr
    │   ├── configuration.cr
    │   ├── dequeue_adapter.cr
    │   ├── dequeue_adapters/
    │   │   ├── concurrency_limited_dequeue_adapter.cr
    │   │   ├── remote_config_dequeue_adapter.cr
    │   │   ├── shuffle_dequeue_adapter.cr
    │   │   └── weighted_dequeue_adapter.cr
    │   ├── exceptions.cr
    │   ├── gates/
    │   │   ├── open_gate.cr
    │   │   └── threshold_gate.cr
    │   ├── job.cr
    │   ├── job_run.cr
    │   ├── key_builder.cr
    │   ├── metadata.cr
    │   ├── periodic_job.cr
    │   ├── periodic_job_run.cr
    │   ├── queue.cr
    │   ├── queued_job.cr
    │   ├── rate_limiter.cr
    │   ├── redis_backend.cr
    │   ├── resource_gate.cr
    │   ├── runnable.cr
    │   ├── runner.cr
    │   ├── runners/
    │   │   ├── coordinator.cr
    │   │   ├── executor.cr
    │   │   ├── idle_wait.cr
    │   │   ├── overseer.cr
    │   │   ├── queue_list.cr
    │   │   └── run_at_most.cr
    │   ├── scheduled_job.cr
    │   ├── serializers/
    │   │   └── primitives.cr
    │   ├── test_backend.cr
    │   ├── unique_job.cr
    │   └── version.cr
    ├── mosquito.cr
    └── ye_olde_redis.cr
Condensed preview — 134 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (336K chars).
[
  {
    "path": ".claude/hooks/session-start.sh",
    "chars": 1354,
    "preview": "#!/bin/bash\nset -euo pipefail\n\n# Only run in remote (cloud) environments\nif [ \"${CLAUDE_CODE_REMOTE:-}\" != \"true\" ]; the"
  },
  {
    "path": ".claude/settings.json",
    "chars": 225,
    "preview": "{\n  \"hooks\": {\n    \"SessionStart\": [\n      {\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \""
  },
  {
    "path": ".claude/todo.md",
    "chars": 4065,
    "preview": "# Migration from publish_metrics branch\n\n## Background\n\nThe `publish_metrics` branch contains observability improvements"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 312,
    "preview": "# These are supported funding model platforms\n\ngithub: robacarp\npatreon: # Replace with a single Patreon username\nopen_c"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.md",
    "chars": 172,
    "preview": "---\nname: Bug\nabout: Mosquito has a bug!\ntitle: ''\nlabels: ''\nassignees: robacarp\n\n---\n\nPlease include some details:\n\nCr"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 873,
    "preview": "name: Test and Demo\non:\n  pull_request:\n    branches:\n      - master\n  push:\n    branches:\n      - master\n\njobs:\n  build"
  },
  {
    "path": ".github/workflows/docs.yml",
    "chars": 526,
    "preview": "name: Build Docs\non:\n  push:\n    branches:\n      - master\n\njobs:\n  deploy:\n    name: Running Docs\n    runs-on: ubuntu-la"
  },
  {
    "path": ".gitignore",
    "chars": 239,
    "preview": "/lib/\n/bin/\n/.shards/\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in application that uses the"
  },
  {
    "path": ".tool-versions",
    "chars": 15,
    "preview": "crystal 1.19.1\n"
  },
  {
    "path": ":w",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 12274,
    "preview": "# Changelog\n\nThe format is based on [Keep a\nChangelog](https://keepachangelog.com/en/1.0.0/), and this project adheres t"
  },
  {
    "path": "LICENSE",
    "chars": 1085,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2019 Robert L Carpenter\n\nPermission is hereby granted, free of charge, to any perso"
  },
  {
    "path": "Makefile",
    "chars": 172,
    "preview": "SHELL=/bin/bash\n\n.PHONY: all\nall: test\n\tshards build\n\n\n.PHONY: test\ntest:\n\tcrystal spec --error-trace -- --chaos\n\n.PHONY"
  },
  {
    "path": "README.md",
    "chars": 5242,
    "preview": "<img src=\"logo/logotype_horizontal.svg\" alt=\"mosquito\">\n\n[![GitHub](https://img.shields.io/github/license/mosquito-cr/mo"
  },
  {
    "path": "benchmark/benchmark.cr",
    "chars": 1850,
    "preview": "require \"../src/mosquito\"\nrequire \"./jobs/*\"\n\nMosquito.configure do |settings|\n  settings.backend_connection_string = EN"
  },
  {
    "path": "benchmark/jobs/emit_message_job.cr",
    "chars": 202,
    "preview": "class EmitMessageJob < Mosquito::QueuedJob\n  PUBSUB_CHANNEL = \"benchmark:messages\"\n  def perform\n    number = Random::Se"
  },
  {
    "path": "demo/jobs/custom_serializers.cr",
    "chars": 399,
    "preview": "class CustomSerializersJob < Mosquito::QueuedJob\n  param count : Int32\n\n  def perform\n    log \"deserialized: #{count}\"\n "
  },
  {
    "path": "demo/jobs/periodically_puts.cr",
    "chars": 314,
    "preview": "class PeriodicallyPuts < Mosquito::PeriodicJob\n  run_every 3.seconds\n\n  queue_name :demo_queue\n\n  def perform\n    log \"H"
  },
  {
    "path": "demo/jobs/queued_job.cr",
    "chars": 256,
    "preview": "class QueuedJob < Mosquito::QueuedJob\n  param count : Int32\n\n  queue_name :demo_queue\n\n  def perform\n    count.times do "
  },
  {
    "path": "demo/jobs/rate_limited_job.cr",
    "chars": 290,
    "preview": "class RateLimitedJob < Mosquito::QueuedJob\n  before do\n    log self.class.rate_limit_stats\n  end\n\n  include Mosquito::Ra"
  },
  {
    "path": "demo/jobs/unique_job.cr",
    "chars": 552,
    "preview": "class UniqueJob < Mosquito::QueuedJob\n  include Mosquito::UniqueJob\n\n  unique_for 1.hour, key: [:user_id]\n\n  param user_"
  },
  {
    "path": "demo/run.cr",
    "chars": 1293,
    "preview": "require \"../src/mosquito\"\n\nMosquito.configure do |settings|\n  settings.backend_connection_string = ENV[\"REDIS_URL\"]? || "
  },
  {
    "path": "scripts/increment_version",
    "chars": 906,
    "preview": "#!/usr/bin/env crystal\n\nrequire \"yaml\"\nrequire \"option_parser\"\n\nshard_yml = \"shard.yml\"\n\nto_increment = \"none\"\n\nOptionPa"
  },
  {
    "path": "scripts/lib/increment_version.sh",
    "chars": 1331,
    "preview": "#!/bin/bash\nset -euo pipefail\nIFS=$'\\n\\t'\n\nprint_help () {\n  cat <<HELP\n$0: increments a version number.\n\nUsage: $0 -h x"
  },
  {
    "path": "scripts/version_tag",
    "chars": 103,
    "preview": "#!/bin/bash\n\nversion=$(\n  grep -e '^version' shard.yml \\\n    | awk '{print \"v\"$2}'\n)\n\ngit tag $version\n"
  },
  {
    "path": "shard.yml",
    "chars": 408,
    "preview": "name: mosquito\nversion: 2.0.0\n\nauthors:\n  - robacarp\n\ncrystal: '>= 1.19'\n\nlicense: MIT\n\ntargets:\n  demo:\n    main: demo/"
  },
  {
    "path": "spec/helpers/bare_base_class.cr",
    "chars": 493,
    "preview": "module Mosquito\n  class Base\n    # Testing wedge which wipes out the JobRun mapping for the\n    # duration of the block."
  },
  {
    "path": "spec/helpers/configuration_helper.cr",
    "chars": 474,
    "preview": "module Mosquito\n  class_setter configuration\n\n  macro temp_config(**settings)\n    original_config = {{ @type }}.configur"
  },
  {
    "path": "spec/helpers/global_helpers.cr",
    "chars": 589,
    "preview": "module TestHelpers\n  extend self\n\n  # Testing wedge which provides a clean slate to ensure tests\n  # aren't dependent on"
  },
  {
    "path": "spec/helpers/logging_helper.cr",
    "chars": 2242,
    "preview": "require \"log\"\n\nclass TestingLogBackend < Log::MemoryBackend\n  def self.instance\n    @@instance ||= new\n  end\n\n  def clea"
  },
  {
    "path": "spec/helpers/mock_coordinator.cr",
    "chars": 533,
    "preview": "class MockCoordinator < Mosquito::Runners::Coordinator\n  getter schedule_count\n\n  def initialize(queue_list : Mosquito::"
  },
  {
    "path": "spec/helpers/mock_executor.cr",
    "chars": 525,
    "preview": "class MockExecutor < Mosquito::Runners::Executor\n  setter work_unit : Mosquito::WorkUnit?\n\n  def state=(state : Mosquito"
  },
  {
    "path": "spec/helpers/mock_overseer.cr",
    "chars": 757,
    "preview": "class MockOverseer < Mosquito::Runners::Overseer\n  property queue_list, coordinator, executors, work_handout, finished_n"
  },
  {
    "path": "spec/helpers/mock_queue_list.cr",
    "chars": 388,
    "preview": "class MockQueueList < Mosquito::Runners::QueueList\n  setter state\n\n  def discovered_queues : Array(Mosquito::Queue)\n    "
  },
  {
    "path": "spec/helpers/mocks.cr",
    "chars": 5046,
    "preview": "# A global place for global mocks\n\nmodule PerformanceCounter\n  def perform\n    self.class.performed!\n  end\n\n  macro incl"
  },
  {
    "path": "spec/helpers/null_dequeue_adapter.cr",
    "chars": 268,
    "preview": "# A test adapter that always returns nil, simulating empty queues.\nclass NullDequeueAdapter < Mosquito::DequeueAdapter\n "
  },
  {
    "path": "spec/helpers/pub_sub.cr",
    "chars": 1293,
    "preview": "module Mosquito::Observability::Publisher\n  @[AlwaysInline]\n  def publish(data : NamedTuple)\n    metrics do\n      Log.de"
  },
  {
    "path": "spec/helpers/spy_dequeue_adapter.cr",
    "chars": 414,
    "preview": "# A test adapter that tracks which queues were checked, in order.\nclass SpyDequeueAdapter < Mosquito::DequeueAdapter\n  g"
  },
  {
    "path": "spec/mosquito/api/executor_config_spec.cr",
    "chars": 4237,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Mosquito::Api::ExecutorConfig\" do\n  describe \"global executor count\" do\n    it \"r"
  },
  {
    "path": "spec/mosquito/api/executor_spec.cr",
    "chars": 2950,
    "preview": "require \"../../spec_helper\"\n\ndescribe Mosquito::Api::Executor do\n  let(executor_pipeline) { Channel(Mosquito::WorkUnit)."
  },
  {
    "path": "spec/mosquito/api/job_run_spec.cr",
    "chars": 1906,
    "preview": "require \"../../spec_helper\"\n\ndescribe Mosquito::Api::JobRun do\n  # the job run timestamps are stored as a unix epoch wit"
  },
  {
    "path": "spec/mosquito/api/overseer_spec.cr",
    "chars": 1640,
    "preview": "require \"../../spec_helper\"\n\ndescribe Mosquito::Api::Overseer do\n  let(:overseer) { MockOverseer.new }\n  let(:api) { Mos"
  },
  {
    "path": "spec/mosquito/api/periodic_job_spec.cr",
    "chars": 2096,
    "preview": "require \"../../spec_helper\"\n\ndescribe Mosquito::Api::PeriodicJob do\n  getter interval : Time::Span = 2.minutes\n\n  descri"
  },
  {
    "path": "spec/mosquito/api/publisher_spec.cr",
    "chars": 879,
    "preview": "require \"../../spec_helper\"\n\ndescribe Mosquito::Api::Publisher do\n  let(executor_pipeline) { Channel(Mosquito::WorkUnit)"
  },
  {
    "path": "spec/mosquito/api/queue_spec.cr",
    "chars": 3267,
    "preview": "require \"../../spec_helper\"\n\ndescribe Mosquito::Api::Queue do\n  let(job_classes) {\n    [QueuedTestJob, PassingJob, Faili"
  },
  {
    "path": "spec/mosquito/api_spec.cr",
    "chars": 512,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::Api do\n  let(queued_test_job) { QueuedTestJob.new }\n  let(passing_job) { Pa"
  },
  {
    "path": "spec/mosquito/backend/deleting_spec.cr",
    "chars": 2000,
    "preview": "\nrequire \"../../spec_helper\"\n\ndescribe \"Backend deleting\" do\n  getter queue_name : String { \"test#{rand(1000)}\" }\n  gett"
  },
  {
    "path": "spec/mosquito/backend/executor_spec.cr",
    "chars": 646,
    "preview": "require \"../../spec_helper\"\n\ndescribe Mosquito::Backend do\n  getter key : String { \"key-#{rand 1000}\" }\n\n  it \"can calcu"
  },
  {
    "path": "spec/mosquito/backend/expiring_list_spec.cr",
    "chars": 724,
    "preview": "require \"../../spec_helper\"\n\ndescribe Mosquito::RedisBackend do\n  describe \"expiring lists\" do\n    it \"can add an item t"
  },
  {
    "path": "spec/mosquito/backend/hash_storage_spec.cr",
    "chars": 885,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Backend hash storage\" do\n  getter sample_data : Hash(String,String) { { \"test\" =>"
  },
  {
    "path": "spec/mosquito/backend/inspection_spec.cr",
    "chars": 3004,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Backend inspection\" do\n  getter backend_name : String { \"test#{rand(1000)}\" }\n  g"
  },
  {
    "path": "spec/mosquito/backend/lock_spec.cr",
    "chars": 1538,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"distributed locking\" do\n  getter key : String { \"testing:backend:lock\" }\n  getter"
  },
  {
    "path": "spec/mosquito/backend/overseer_spec.cr",
    "chars": 742,
    "preview": "require \"../../spec_helper\"\n\ndescribe Mosquito::Backend do\n  it \"can keep a list of overseers\" do\n    clean_slate do\n   "
  },
  {
    "path": "spec/mosquito/backend/queueing_spec.cr",
    "chars": 4758,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Backend Queues\" do\n  getter backend_name : String { \"test#{rand(1000)}\" }\n  gette"
  },
  {
    "path": "spec/mosquito/backend_spec.cr",
    "chars": 970,
    "preview": "require \"../spec_helper\"\n\n# These tests are explicitly for code which is inherited from the abstract Backend\ndescribe Mo"
  },
  {
    "path": "spec/mosquito/base_spec.cr",
    "chars": 488,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::Base do\n  it \"keeps a list of scheduled job_runs\" do\n    Base.bare_mapping "
  },
  {
    "path": "spec/mosquito/configuration_spec.cr",
    "chars": 2266,
    "preview": "require \"../spec_helper\"\n\ndescribe \"Mosquito Config\" do\n  it \"allows setting / retrieving the connection string\" do\n    "
  },
  {
    "path": "spec/mosquito/dequeue_adapters/concurrency_limited_dequeue_adapter_spec.cr",
    "chars": 4839,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Mosquito::ConcurrencyLimitedDequeueAdapter\" do\n  getter(overseer : MockOverseer) "
  },
  {
    "path": "spec/mosquito/dequeue_adapters/remote_config_dequeue_adapter_spec.cr",
    "chars": 13620,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Mosquito::RemoteConfigDequeueAdapter\" do\n  getter(overseer : MockOverseer) { Mock"
  },
  {
    "path": "spec/mosquito/dequeue_adapters/shuffle_dequeue_adapter_spec.cr",
    "chars": 2482,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Mosquito::ShuffleDequeueAdapter\" do\n  getter(overseer : MockOverseer) { MockOvers"
  },
  {
    "path": "spec/mosquito/dequeue_adapters/weighted_dequeue_adapter_spec.cr",
    "chars": 3189,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Mosquito::WeightedDequeueAdapter\" do\n  getter(overseer : MockOverseer) { MockOver"
  },
  {
    "path": "spec/mosquito/exceptions_spec.cr",
    "chars": 302,
    "preview": "require \"../spec_helper\"\n\ndescribe \"Mosquito exceptions\" do\n  it \"declares JobFailed\" do\n    Mosquito::JobFailed.new \"te"
  },
  {
    "path": "spec/mosquito/job/job_state_spec.cr",
    "chars": 579,
    "preview": "require \"../../spec_helper\"\n\ndescribe Mosquito::Job::State do\n  describe \"executed?\" do\n    it \"Marks jobs as executed w"
  },
  {
    "path": "spec/mosquito/job_run/rescheduling_spec.cr",
    "chars": 2748,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"job_run rescheduling\" do\n  @failing_job_run : Mosquito::JobRun?\n  getter failing_"
  },
  {
    "path": "spec/mosquito/job_run/running_spec.cr",
    "chars": 1447,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"job_run running\" do\n  # the job run timestamps are stored as a unix epoch with mi"
  },
  {
    "path": "spec/mosquito/job_run/storage_spec.cr",
    "chars": 3481,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"job_run storage\" do\n  getter backend : Mosquito::Backend::Queue = Mosquito.backen"
  },
  {
    "path": "spec/mosquito/job_run_spec.cr",
    "chars": 47,
    "preview": "require \"../spec_helper\"\nrequire \"./job_run/*\"\n"
  },
  {
    "path": "spec/mosquito/job_spec.cr",
    "chars": 4230,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::Job do\n  getter(passing_job) { PassingJob.new }\n  getter(failing_job) { Fai"
  },
  {
    "path": "spec/mosquito/key_builder_spec.cr",
    "chars": 577,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::KeyBuilder do\n  it \"builds keys from tuples\" do\n    assert_equal \"fizz:buzz"
  },
  {
    "path": "spec/mosquito/metadata_spec.cr",
    "chars": 3130,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::Metadata do\n  getter(store_name : String) { \"test_store#{rand 1000}\" }\n  ge"
  },
  {
    "path": "spec/mosquito/periodic_job_run_spec.cr",
    "chars": 3989,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::PeriodicJobRun do\n  getter interval : Time::Span = 2.minutes\n\n  it \"tries t"
  },
  {
    "path": "spec/mosquito/periodic_job_spec.cr",
    "chars": 1144,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::PeriodicJob do\n  getter(runner) { Mosquito::TestableRunner.new }\n\n  it \"cor"
  },
  {
    "path": "spec/mosquito/queue_spec.cr",
    "chars": 5813,
    "preview": "require \"../spec_helper\"\n\ndescribe Queue do\n  getter(name) { \"test#{rand(1000)}\" }\n\n  getter(test_queue) do\n    Mosquito"
  },
  {
    "path": "spec/mosquito/queued_job_spec.cr",
    "chars": 2532,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::QueuedJob do\n  getter(runner) { Mosquito::TestableRunner.new }\n  getter(nam"
  },
  {
    "path": "spec/mosquito/rate_limiter_spec.cr",
    "chars": 4811,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::RateLimiter do\n  describe \"RateLimiter.rate_limit_stats\" do\n    it \"provide"
  },
  {
    "path": "spec/mosquito/resource_gate_spec.cr",
    "chars": 1644,
    "preview": "require \"../spec_helper\"\n\ndescribe \"Mosquito::OpenGate\" do\n  it \"always allows\" do\n    gate = Mosquito::OpenGate.new\n   "
  },
  {
    "path": "spec/mosquito/runnable_spec.cr",
    "chars": 1610,
    "preview": "require \"../spec_helper\"\n\nclass Namespace::ConcreteRunnable\n  include Mosquito::Runnable\n\n  getter first_run_notifier = "
  },
  {
    "path": "spec/mosquito/runners/coordinator_spec.cr",
    "chars": 4768,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Mosquito::Runners::Coordinator\" do\n  getter(queue : Queue) { test_job.class.queue"
  },
  {
    "path": "spec/mosquito/runners/executor_spec.cr",
    "chars": 5812,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Mosquito::Runners::Executor\" do\n  getter(queue_list) { MockQueueList.new }\n  gett"
  },
  {
    "path": "spec/mosquito/runners/overseer_spec.cr",
    "chars": 9211,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Mosquito::Runners::Overseer\" do\n  getter(overseer : MockOverseer) { MockOverseer."
  },
  {
    "path": "spec/mosquito/runners/queue_list_spec.cr",
    "chars": 4989,
    "preview": "require \"../../spec_helper\"\n\ndescribe \"Mosquito::Runners::QueueList\" do\n  getter(queue_list) { MockQueueList.new }\n\n  de"
  },
  {
    "path": "spec/mosquito/runners/run_at_most_spec.cr",
    "chars": 1337,
    "preview": "require \"../../spec_helper\"\n\nclass RunsAtMostMock\n  include Mosquito::Runners::RunAtMost\n\n  def yield_once_a_second(&blo"
  },
  {
    "path": "spec/mosquito/serializers/primitive_serializers_spec.cr",
    "chars": 451,
    "preview": "require \"uuid\"\nrequire \"../../spec_helper\"\n\nclass PrimitiveSerializerTester\n  extend Mosquito::Serializers::Primitives\ne"
  },
  {
    "path": "spec/mosquito/testing_backend_spec.cr",
    "chars": 1194,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::TestBackend do\n  def latest_enqueued_job\n    Mosquito::TestBackend.enqueued"
  },
  {
    "path": "spec/mosquito/unique_job_spec.cr",
    "chars": 5422,
    "preview": "require \"../spec_helper\"\n\ndescribe Mosquito::UniqueJob do\n  describe \"first enqueue\" do\n    it \"enqueues a job when no d"
  },
  {
    "path": "spec/mosquito/version_spec.cr",
    "chars": 300,
    "preview": "require \"../spec_helper\"\nrequire \"yaml\"\n\ndescribe \"mosquito version numbers\" do\n  it \"is defined\" do\n    assert Mosquito"
  },
  {
    "path": "spec/spec_helper.cr",
    "chars": 415,
    "preview": "require \"minitest\"\nrequire \"minitest/focus\"\n\nrequire \"log\"\nLog.setup :fatal\n\nrequire \"timecop\"\nTimecop.safe_mode = true\n"
  },
  {
    "path": "src/mosquito/api/concurrency_config.cr",
    "chars": 1969,
    "preview": "module Mosquito\n  # Provides read/write access to the remotely stored concurrency limits\n  # used by `RemoteConfigDequeu"
  },
  {
    "path": "src/mosquito/api/executor.cr",
    "chars": 5732,
    "preview": "module Mosquito\n  module Api\n    # An interface for an executor.\n    #\n    # This is used to inspect the state of an exe"
  },
  {
    "path": "src/mosquito/api/executor_config.cr",
    "chars": 3498,
    "preview": "module Mosquito\n  # Provides read/write access to the remotely stored executor count\n  # used by overseers configured wi"
  },
  {
    "path": "src/mosquito/api/job_run.cr",
    "chars": 1591,
    "preview": "module Mosquito::Api\n  # Represents a job run in Mosquito.\n  #\n  # This class is used to inspect a job run stored in the"
  },
  {
    "path": "src/mosquito/api/observability/publisher.cr",
    "chars": 949,
    "preview": "module Mosquito::Observability::Publisher\n  Log = ::Log.for(\"mosquito.events\")\n\n  getter publish_context : PublishContex"
  },
  {
    "path": "src/mosquito/api/overseer.cr",
    "chars": 3941,
    "preview": "module Mosquito\n  # An interface for inspecting the state of Mosquito Overseers.\n  #\n  # For more information about over"
  },
  {
    "path": "src/mosquito/api/periodic_job.cr",
    "chars": 1795,
    "preview": "module Mosquito\n  # An interface for inspecting the state of periodic jobs.\n  #\n  # This class provides read-only access"
  },
  {
    "path": "src/mosquito/api/queue.cr",
    "chars": 3844,
    "preview": "module Mosquito\n  # Represents a named queue in the system, and allows querying the state of the queue. For more about t"
  },
  {
    "path": "src/mosquito/api/queue_list.cr",
    "chars": 678,
    "preview": "module Mosquito\n  class Observability::QueueList\n    private getter log : ::Log\n    @last_paused_names = Set(String).new"
  },
  {
    "path": "src/mosquito/api.cr",
    "chars": 2746,
    "preview": "require \"./backend\"\nrequire \"./api/observability/*\"\nrequire \"./api/*\"\n\nmodule Mosquito::Api\n  def self.overseer(id : Str"
  },
  {
    "path": "src/mosquito/backend.cr",
    "chars": 3698,
    "preview": "module Mosquito\n  abstract class Backend\n    struct BroadcastMessage\n      property channel : String\n      property mess"
  },
  {
    "path": "src/mosquito/base.cr",
    "chars": 1217,
    "preview": "require \"json\"\n\nmodule Mosquito\n  alias Id = Int64 | Int32\n  record WorkUnit, job_run : JobRun, queue : Queue do\n    def"
  },
  {
    "path": "src/mosquito/configuration.cr",
    "chars": 2585,
    "preview": "module Mosquito\n  class_getter configuration = Configuration.new\n\n  def self.configure(&block) : Nil\n    yield configura"
  },
  {
    "path": "src/mosquito/dequeue_adapter.cr",
    "chars": 1211,
    "preview": "module Mosquito\n  # A DequeueAdapter determines how the Overseer selects the next job to\n  # execute from the available "
  },
  {
    "path": "src/mosquito/dequeue_adapters/concurrency_limited_dequeue_adapter.cr",
    "chars": 2255,
    "preview": "require \"../dequeue_adapter\"\n\nmodule Mosquito\n  # A dequeue adapter that enforces per-queue concurrency limits.\n  #\n  # "
  },
  {
    "path": "src/mosquito/dequeue_adapters/remote_config_dequeue_adapter.cr",
    "chars": 6093,
    "preview": "require \"./concurrency_limited_dequeue_adapter\"\n\nmodule Mosquito\n  # A dequeue adapter that wraps `ConcurrencyLimitedDeq"
  },
  {
    "path": "src/mosquito/dequeue_adapters/shuffle_dequeue_adapter.cr",
    "chars": 557,
    "preview": "require \"../dequeue_adapter\"\n\nmodule Mosquito\n  # The default dequeue adapter. Shuffles the queue list on each pass and\n"
  },
  {
    "path": "src/mosquito/dequeue_adapters/weighted_dequeue_adapter.cr",
    "chars": 2229,
    "preview": "require \"../dequeue_adapter\"\n\nmodule Mosquito\n  # A dequeue adapter that checks queues according to configured weights.\n"
  },
  {
    "path": "src/mosquito/exceptions.cr",
    "chars": 331,
    "preview": "module Mosquito\n  # When a job fails\n  class JobFailed < Exception\n  end\n\n  # When a job_run tries to run twice\n  class "
  },
  {
    "path": "src/mosquito/gates/open_gate.cr",
    "chars": 307,
    "preview": "require \"../resource_gate\"\n\nmodule Mosquito\n  # A gate that always allows dequeuing. This is the default when no\n  # res"
  },
  {
    "path": "src/mosquito/gates/threshold_gate.cr",
    "chars": 711,
    "preview": "require \"../resource_gate\"\n\nmodule Mosquito\n  # A gate that samples a metric via a callback and compares it against\n  # "
  },
  {
    "path": "src/mosquito/job.cr",
    "chars": 5503,
    "preview": "require \"./serializers/*\"\n\nmodule Mosquito\n  # A Job is a definition for work to be performed.\n  # Jobs are pieces of co"
  },
  {
    "path": "src/mosquito/job_run.cr",
    "chars": 5812,
    "preview": "module Mosquito\n  # A JobRun is a unit of work which will be performed by a Job.\n  # JobRuns know how to:\n  # - store an"
  },
  {
    "path": "src/mosquito/key_builder.cr",
    "chars": 657,
    "preview": "module Mosquito\n  class KeyBuilder\n    KEY_SEPERATOR = \":\"\n\n    def self.build(*parts)\n      id = [] of String\n\n      pa"
  },
  {
    "path": "src/mosquito/metadata.cr",
    "chars": 2979,
    "preview": "module Mosquito\n  # Provides a real-time metadata store. Data is not cached, which allows\n  # multiple workers to operat"
  },
  {
    "path": "src/mosquito/periodic_job.cr",
    "chars": 605,
    "preview": "module Mosquito\n  abstract class PeriodicJob < Job\n    def initialize\n    end\n\n    abstract def build_job_run\n\n    macro"
  },
  {
    "path": "src/mosquito/periodic_job_run.cr",
    "chars": 2937,
    "preview": "module Mosquito\n  class PeriodicJobRun\n    Log = ::Log.for self\n\n    property class : Mosquito::PeriodicJob.class\n    pr"
  },
  {
    "path": "src/mosquito/queue.cr",
    "chars": 4630,
    "preview": "module Mosquito\n  # A named Queue.\n  #\n  # Named Queues exist and have 4 ordered lists: waiting, pending, scheduled, and"
  },
  {
    "path": "src/mosquito/queued_job.cr",
    "chars": 6100,
    "preview": "module Mosquito\n  abstract class QueuedJob < Job\n    macro inherited\n      def self.job_name\n        \"{{ @type.id }}\".un"
  },
  {
    "path": "src/mosquito/rate_limiter.cr",
    "chars": 4043,
    "preview": "module Mosquito::RateLimiter\n  module ClassMethods\n    # Configures rate limiting for this job.\n    #\n    # `limit` and "
  },
  {
    "path": "src/mosquito/redis_backend.cr",
    "chars": 10852,
    "preview": "require \"redis\"\nrequire \"digest/sha1\"\n\nmodule Mosquito\n  module Scripts\n    SCRIPTS = {\n      :remove_matching_key => <<"
  },
  {
    "path": "src/mosquito/resource_gate.cr",
    "chars": 1582,
    "preview": "module Mosquito\n  # A ResourceGate controls whether work should be dequeued based on\n  # external resource availability "
  },
  {
    "path": "src/mosquito/runnable.cr",
    "chars": 5676,
    "preview": "require \"wait_group\"\n\nmodule Mosquito\n  # Runnable implements a general purpose spawn/loop which carries a state\n  # enu"
  },
  {
    "path": "src/mosquito/runner.cr",
    "chars": 2203,
    "preview": "require \"colorize\"\n\nmodule Mosquito\n  # This singleton class serves as a shorthand for starting and managing an Overseer"
  },
  {
    "path": "src/mosquito/runners/coordinator.cr",
    "chars": 2259,
    "preview": "module Mosquito::Runners\n  # primer? loader? _scheduler_\n  class Coordinator\n    Log = ::Log.for self\n    LockTTL = 30.s"
  },
  {
    "path": "src/mosquito/runners/executor.cr",
    "chars": 4850,
    "preview": "require \"./run_at_most\"\nrequire \"../runnable\"\n\nmodule Mosquito::Runners\n  # The executor is the center of work in Mosqui"
  },
  {
    "path": "src/mosquito/runners/idle_wait.cr",
    "chars": 276,
    "preview": "module Mosquito::Runners\n  module IdleWait\n    def with_idle_wait(idle_wait : Time::Span)\n      delta = Time.measure do\n"
  },
  {
    "path": "src/mosquito/runners/overseer.cr",
    "chars": 10918,
    "preview": "require \"./idle_wait\"\nrequire \"./queue_list\"\nrequire \"./run_at_most\"\nrequire \"../runnable\"\n\nmodule Mosquito::Runners\n  #"
  },
  {
    "path": "src/mosquito/runners/queue_list.cr",
    "chars": 3145,
    "preview": "require \"./run_at_most\"\nrequire \"../runnable\"\nrequire \"./idle_wait\"\nrequire \"../resource_gate\"\n\nmodule Mosquito::Runners"
  },
  {
    "path": "src/mosquito/runners/run_at_most.cr",
    "chars": 409,
    "preview": "module Mosquito::Runners\n  module RunAtMost\n    getter execution_timestamps = {} of Symbol => Time::Instant\n\n    private"
  },
  {
    "path": "src/mosquito/scheduled_job.cr",
    "chars": 451,
    "preview": "module Mosquito\n  abstract class ScheduledJob < Job\n    def initialize\n    end\n\n    abstract def build_job_run\n\n    macr"
  },
  {
    "path": "src/mosquito/serializers/primitives.cr",
    "chars": 1594,
    "preview": "module Mosquito::Serializers::Primitives\n  def serialize_string(str : String) : String\n    str\n  end\n\n  def deserialize_"
  },
  {
    "path": "src/mosquito/test_backend.cr",
    "chars": 5353,
    "preview": "module Mosquito\n  # An in-memory noop backend desigend to be used in application testing.\n  #\n  # The test mode backend "
  },
  {
    "path": "src/mosquito/unique_job.cr",
    "chars": 3022,
    "preview": "module Mosquito::UniqueJob\n  module ClassMethods\n    # Configures job uniqueness for this job.\n    #\n    # `duration` co"
  },
  {
    "path": "src/mosquito/version.cr",
    "chars": 40,
    "preview": "module Mosquito\n  VERSION = \"2.0.0\"\nend\n"
  },
  {
    "path": "src/mosquito.cr",
    "chars": 187,
    "preview": "require \"./mosquito/runners/run_at_most\"\n\nrequire \"./mosquito/api\"\nrequire \"./mosquito/**\"\n\nmodule Mosquito\n  Log = ::Lo"
  },
  {
    "path": "src/ye_olde_redis.cr",
    "chars": 389,
    "preview": "# Monkeypatch to revert to the old Redis behavior, for Redis servers pre 6.2 which don't support\n# https://redis.io/docs"
  }
]

About this extraction

This page contains the full source code of the robacarp/mosquito GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 134 files (306.2 KB), approximately 83.0k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!