Skip to content

ReadOptions.runTime is unreachable in Lambda — auto-derived stopTime pre-empts it #226

Description

@mecham-lynn

Summary

ReadOptions.runTime is documented and typed, but is unreachable whenever the SDK runs in a Lambda — the auto-derived stopTime pre-empts it. Confirmed identical in 7.1.18 and the current 7.1.21.

The code

lib/stream/leo-stream.js, fromLeo (7.1.21, lines 1355-1363):

fromLeo: (ID, queue, opts) => {
    opts = Object.assign({}, opts || {});
    queue = refUtil.ref(queue).queue(opts.subqueue).id;
    if (!opts.stopTime && configure.registry && configure.registry.context) {
        opts.stopTime = moment.now() + (configure.registry.context.getRemainingTimeInMillis() * 0.8);
    }
    if (!opts.stopTime && opts.runTime) {          // <-- unreachable in Lambda
        opts.stopTime = moment().add(opts.runTime).valueOf();
    }

In a Lambda, configure.registry.context is always present, so the first branch always assigns opts.stopTime. The second branch is guarded on !opts.stopTime and therefore never executes. A bot passing runTime gets the hardcoded 0.8 × getRemainingTimeInMillis() instead, silently.

Why this is surprising

lib/lib.d.ts documents runTime as the intended knob for exactly this, and even prescribes the ratio the hardcoded value implements:

The duration of time the to read for before closing the read stream. It is common to set this to 75% to 80% of the time remaining before the lambda is shut down to give the lambda sufficient time to finish processing. Of course, different types of processing will differ.

The read stream will shutdown as soon as one of the constraints is met: runTime, loops, limit, size, stopTime.

"Of course, different types of processing will differ" is the case that cannot currently be expressed. A bot whose per-batch processing is expensive wants a ratio below 0.8 so a batch admitted just under the deadline can still finish and checkpoint. Passing runTime: '5m' to a 15-minute Lambda looks like it does that and does nothing.

There's no config for the ratio either — 0.8 is a literal with no override; runTime is the only documented way to influence it.

Repro

// in a Lambda with a 900s timeout
sdk.enrichEvents({
    id: botId, inQueue: 'q-in', outQueue: 'q-out',
    config: { runTime: '5m' },
    transform: (payload, meta, done) => done(null, true),
});

Expected: read stream closes at ~300s.
Actual: closes at ~720s (0.8 × 900s). logger.info(opts) on line 1365 shows stopTime already set and runTime ignored.

Workaround

Pass stopTime explicitly, since it's the value the first branch guards on:

config: { stopTime: Date.now() + context.getRemainingTimeInMillis() * 0.5 }

Suggested fix

Let an explicitly-passed runTime win over the derived default, e.g.:

if (!opts.stopTime && opts.runTime) {
    opts.stopTime = moment().add(opts.runTime).valueOf();
}
if (!opts.stopTime && configure.registry && configure.registry.context) {
    opts.stopTime = moment.now() + (configure.registry.context.getRemainingTimeInMillis() * 0.8);
}

i.e. swap the two blocks so the caller's explicit option takes precedence and the context-derived value stays the fallback. That preserves current behavior for every bot that doesn't pass runTime.

Optionally, also expose the ratio (e.g. runTimePercent) so bots can tune the reserve without computing an absolute epoch time.

Environment

  • leo-sdk 7.1.18 and 7.1.21
  • Node 22, AWS Lambda

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions