EV-Gearman

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

The handle binds the per-job event callbacks to the right submission
even when many submissions are in flight at once.

Worker lifecycle:

    CAN_DO(func)         -->                              # advertise
                         repeat ----------------------+
    GRAB_JOB             -->                          |
                         <--   JOB_ASSIGN(h,fn,wl)    |   # got work
                                 ... user callback   |
    WORK_COMPLETE(h, r)  -->                          |
                         -------------- or -----------+
                         <--   NO_JOB                 |
    PRE_SLEEP            -->                          |
                         <--   NOOP                   |   # wake-up
                         ----------------------------+

`EV::Gearman` drives that state machine in C; the per-function
callback only sees a ready-to-process [EV::Gearman::Job](https://metacpan.org/pod/EV%3A%3AGearman%3A%3AJob).

# ENCODING

All function names, payloads, results, and handles are byte strings
on the wire. Encode UTF-8 yourself before passing data in:

    use Encode;
    $cli->submit_job(reverse => encode_utf8($str), sub {
        my $result = decode_utf8($_[0] // '');
        ...
    });

Workload and result values can contain arbitrary bytes including
embedded NULs — Gearman's framing puts the payload last in the
packet and uses the header's length field, so it is not NUL-bounded.

# CALLBACK CONVENTIONS

Every command callback receives `($result, $err)`. On success
`$err` is `undef`; on failure `$err` is a string like
`"disconnected"`, `"job failed"`, `"command timeout"`, or text
forwarded from the server (e.g. `"INVALID_FUNCTION_NAME: ..."`).

Callback exceptions are caught with `G_EVAL` and surfaced via
`warn` so a stray `die` from your code never unwinds the libev
event loop. Use `EV::break` to abort the loop deliberately.

# CONSTRUCTOR

## new(%options)

    my $g = EV::Gearman->new(
        host             => '127.0.0.1',
        port             => 4730,
        on_error         => sub { warn "@_" },
        on_connect       => sub { ... },
        on_disconnect    => sub { ... },
        connect_timeout  => 5_000,    # ms
        command_timeout  => 30_000,   # ms
        reconnect        => 1,
        reconnect_delay  => 1000,     # ms
        keepalive        => 60,       # seconds (TCP only)
        exceptions       => 1,        # request "exceptions" option
        client_id        => "worker-$$",
        grab_unique      => 1,        # use GRAB_JOB_UNIQ
    );

If `host` (or `path`) is given, a non-blocking connect starts
immediately. With neither, the object is unconfigured; call
`$g->connect` / `$g->connect_unix` later.

All keys default to `undef` unless noted. Booleans accept any Perl
truthy value.

### Connection

- `host => $str`
- `port => $int`

    TCP host and port. Default port: `4730`. Mutually exclusive with
    `path`.

    Name resolution is currently synchronous: a non-numeric `host` is
    passed straight to `getaddrinfo`, which can block the event loop
    for the system resolver timeout. Pass an IP literal (or pre-resolve
    once) to keep reconnect cycles fully non-blocking.

- `path => $str`

    Unix-domain socket path. Mutually exclusive with `host`.

    Note that a `connect(2)` to a unix socket completes inline, so the
    connection is fully established (and `on_connect` has already fired)
    before `new` returns — a `$g->on_connect(...)` assigned after
    construction will never fire for it. Pass `on_connect` to the
    constructor instead (the same can theoretically happen for a TCP
    connect that completes immediately).

- `loop => $ev_loop`

    EV loop to attach to. Default: `EV::default_loop`.

- `priority => $num`

    EV watcher priority in `-2 .. +2`. Higher = serviced before other
    EV watchers in the same iteration. Default `0`.

- `keepalive => $seconds`

    TCP keepalive idle interval. `0` disables. Ignored on Unix sockets.

### Timeouts

- `connect_timeout => $ms`

    Abort an in-progress non-blocking connect after this many ms. `0`
    &#x3d; no timeout (default).

- `command_timeout => $ms`

    Per-request timeout. The request at the head of the pending queue is
    given this many ms from the moment it is written to the socket; if it
    is still unanswered when its budget expires, the connection is torn
    down with `"command timeout"`. The budget is independent of
    unrelated traffic: other packets arriving meanwhile neither extend
    the head request's budget nor shorten it, and once the head is
    answered the next request's own budget applies. A slow reply that
    keeps dribbling in is therefore safe as long as the request completes
    within its budget — and a genuinely stuck request dies on schedule
    even on an otherwise busy connection. `0` = no timeout (default).

### Reconnect

- `reconnect => $bool`

    Enable automatic reconnect on transport errors.

- `reconnect_delay => $ms`

    Wait this many ms before each reconnect attempt. Default `1000`.
    The delay is always honored via a timer, so even `0` defers
    through the event loop (no synchronous retry recursion).

- `max_reconnect_attempts => $num`

    Give up after this many consecutive failures and emit
    `"max reconnect attempts reached"`. `0` = unlimited (default).

After a reconnect, all worker `CAN_DO`/`CAN_DO_TIMEOUT`
registrations and the `exceptions` option are re-sent
automatically.

### Worker / option flags

- `exceptions => $bool`

    If true, the `exceptions` option is sent on every connect, so
    foreground clients receive `WORK_EXCEPTION` packets. For workers,
    this also enables forwarding `die` messages from sync callbacks
    as exceptions instead of the `WORK_FAIL` (`WORK_EXCEPTION` is
    terminal at the server; sending both would earn a `JOB_NOT_FOUND`).

- `client_id => $str`

    Sent as `SET_CLIENT_ID` on every connect. Visible in the admin
    `workers` output.

- `grab_unique => $bool`

    If true, the worker GRAB loop uses `GRAB_JOB_UNIQ`, so the job

README.md  view on Meta::CPAN

gearmand answers a single-line `ERR` instead); everything else is
treated as single-line.

A single-line reply beginning with `ERR ` (gearmand's text-protocol
error prefix, e.g. for an unrecognized command) is delivered as the
error argument: `$cb->(undef, "ERR UNKNOWN_COMMAND ...")` — not
as a successful result. The connection stays up.

If a command not in the multi-line set above nevertheless gets a
`".\n"`-terminated multi-line reply from the server, only the first
line is delivered; the leftover lines cannot be attributed to any
request and the connection is torn down with a protocol error.

Because the text protocol has no length prefix, a buffered reply is
capped at 16 MiB: a peer that keeps streaming without ever sending
the terminator is dropped with an `"admin response too large"`
error (and the normal reconnect logic applies).

## server\_status(\[$cb\])

Tab-separated lines: `FUNC \t TOTAL_JOBS \t RUNNING_JOBS \t WORKERS`.

## server\_workers(\[$cb\])

One line per connected worker.

## server\_version(\[$cb\])

Single-line reply (e.g. `"OK 1.1.21+ds"`).

## maxqueue($func, $size, \[$cb\])

Set the per-function queue size cap. Reply is `"OK\n"`.

# INTROSPECTION

## pending\_count

Number of requests sent and awaiting a response — binary client
requests, admin (text-protocol) commands, and the worker's in-flight
`GRAB_JOB` all count.

## waiting\_count

Number of requests held in the local pre-connect queue.

## active\_count

Number of foreground jobs whose handle has been received but
which haven't yet completed.

# ACCESSORS

These tunables have a getter / setter of the same name. Calling
without arguments reads the current value; with one argument, writes
and (where meaningful) takes effect immediately:

    $g->connect_timeout($ms);
    $g->command_timeout($ms);
    $g->priority($num);
    $g->keepalive($seconds);
    $g->on_error($cb);         # set; pass undef to clear
    $g->on_connect($cb);
    $g->on_disconnect($cb);

The remaining `new` options (`host`, `port`, `path`,
`exceptions`, `client_id`, `grab_unique`, ...) are set once at
construction and have no accessor.

`reconnect` is the exception — it is a setter only; pass `0`/`1`
plus optional new delay and attempt cap:

    $g->reconnect($enable, [$delay_ms], [$max_attempts]);

Omitting `$delay_ms` / `$max_attempts` leaves the previously
configured values unchanged.

## reconnect\_enabled

The getter that `reconnect` lacks: returns true while automatic
reconnect is enabled, false otherwise. Takes no arguments.

# LIFECYCLE AND DESTRUCTION

When a connection drops, the FIFO of pending requests is drained
with `(undef, "disconnected")`; foreground active jobs are drained
with the same error. Reconnect (if enabled) re-runs the connect
sequence and re-registers worker abilities.

When the `EV::Gearman` object goes out of scope, every pending
and active callback fires once with `(undef, "disconnected")`,
then the FD is closed. The clean-shutdown idiom is:

    $g->disconnect;            # drains queues, fires on_disconnect
    undef $g;

If callbacks close over `$g` (a common mistake — every reference
inside a closure keeps the object alive), break the cycle first:

    $g->on_error(undef);
    $g->on_connect(undef);
    $g->on_disconnect(undef);
    undef $g;

DESTROY is reentrancy-safe: if a callback fired during teardown
drops the last external reference to a separate `EV::Gearman`,
that object's DESTROY is correctly deferred and run once unwound.

# PERFORMANCE

Loopback benchmark on Linux, Perl 5.40, gearmand 1.1.21, single
worker (always [EV::Gearman](https://metacpan.org/pod/EV%3A%3AGearman) so the worker isn't the bottleneck).

`bench/benchmark.pl` measures one client by itself:

                                     ops/sec
    Pipelined foreground jobs        ~53,000
    Sequential round-trip            ~19,000
    Background submissions          ~280,000

`bench/vs.pl` compares against the existing CPAN clients
([Gearman::Client](https://metacpan.org/pod/Gearman%3A%3AClient) 2.004.015 sync, [AnyEvent::Gearman](https://metacpan.org/pod/AnyEvent%3A%3AGearman) 0.10
async). Numbers in operations / second:

                          EV::Gearman   AnyEvent::Gearman   Gearman::Client
    pipelined foreground   ~51,000          ~5,200              n/a (1)
    sequential round-trip  ~19,000          ~5,900             ~5,400
    background submits    ~248,000          ~5,400              n/a (1)

    (1) Gearman::Client is synchronous — it has neither pipelining
        nor concurrent background submits.

EV::Gearman is roughly **10x** the foreground throughput of
AnyEvent::Gearman, **45x** the background submission rate, and
**3x** the sequential round-trip rate. The gap comes from three
places:

- Pipelining is the default. Submitting N jobs in a tight loop
ships them in batched writes; responses are demultiplexed by
handle as they stream back. AnyEvent::Gearman is async but
serializes one request per round-trip, so it pays a full RTT per
job.
- The protocol implementation is C/XS — packet encode/decode,
buffer growth, and FIFO bookkeeping run without per-call Perl
allocations.
- The IO layer is direct `ev_io` on the gearmand socket, so each
read/write involves no AnyEvent guard-object construction or
backend-dispatch overhead.

Background submissions are particularly fast because the
JOB\_CREATED reply is the only round-trip — no work events to
demultiplex — so the limit is just network latency and parser
throughput.

Sequential round-trip throughput is the worst case: each job
waits for its own reply before the next is built, so pipelining
buys nothing. EV::Gearman is still ~3x faster here purely from
the C-side protocol parser.



( run in 1.258 second using v1.01-cache-2.11-cpan-14f38c9f855 )