Skip to content

[Bug]: grep timeBudgetMs is ignored for zero-match searches #826

Description

@henrychu04

Which fff frontend?

Node SDK (@ff-labs/fff-node)

has logs

No response

Description

Summary

FileFinder.grep() does not enforce timeBudgetMs when a plain-text search produces no matches.

In the reproduction below, a 5 ms search scans all 10,000 candidate files, returns nextCursor: null, and takes substantially longer than the requested budget.

Because the Node API is synchronous and exposes no cancellation mechanism, callers cannot independently interrupt the native operation. This makes timeBudgetMs the only available execution bound.

Environment

Reproduction

mkdir fff-budget-repro
cd fff-budget-repro
npm init -y
npm install @ff-labs/fff-node@0.10.5

Save the following as repro.mjs:

import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { FileFinder } from "@ff-labs/fff-node";

function unwrap(result) {
  if (!result.ok) {
    throw new Error(`FFF operation failed: ${result.error}`);
  }
  return result.value;
}

const root = await fs.mkdtemp(
  path.join(os.tmpdir(), "fff-no-match-budget-"),
);

let finder;

try {
  const contents = `${"x".repeat(4 * 1024)}\n`;

  for (let batch = 0; batch < 100; batch += 1) {
    await Promise.all(
      Array.from({ length: 100 }, (_, index) =>
        fs.writeFile(
          path.join(root, `${batch}-${index}.txt`),
          contents,
        ),
      ),
    );
  }

  finder = unwrap(
    FileFinder.create({
      basePath: root,
      disableMmapCache: true,
      disableContentIndexing: false,
      aiMode: false,
      enableFsRootScanning: false,
      enableHomeDirScanning: false,
    }),
  );

  const ready = unwrap(await finder.waitForIndexReady(30_000));
  if (!ready) {
    throw new Error("Index did not become ready");
  }

  const startedAt = performance.now();

  const result = unwrap(
    finder.grep("needle-not-present", {
      mode: "plain",
      maxFileSize: 1024 * 1024,
      pageSize: 500,
      timeBudgetMs: 5,
    }),
  );

  const elapsedMs = performance.now() - startedAt;

  console.log({
    elapsedMs,
    totalFiles: result.totalFiles,
    filteredFileCount: result.filteredFileCount,
    totalFilesSearched: result.totalFilesSearched,
    nextCursor: result.nextCursor,
    cancellationAvailable: "cancel" in finder,
  });
} finally {
  finder?.destroy();
  await fs.rm(root, { recursive: true, force: true });
}

Run:

node repro.mjs

Actual behavior

Example result from Linux x86-64:

{
  elapsedMs: 59.25,
  totalFiles: 10000,
  filteredFileCount: 10000,
  totalFilesSearched: 10000,
  nextCursor: null,
  cancellationAvailable: false
}

The exact elapsed time is hardware-dependent, but the important behavior is that all candidate files are searched and no continuation cursor is returned after the time budget expires.

Expected behavior

When timeBudgetMs expires:

  • Grep should stop traversing candidate files even if it has found zero matches.
  • totalFilesSearched should be less than filteredFileCount.
  • nextCursor should allow the caller to resume from the next unsearched file, or the result should otherwise explicitly indicate that the search timed out.
  • The synchronous native call should return within a small, documented amount of overshoot beyond the requested budget.

Suspected root cause

The plain/regex grep budget check is conditional on existing matches:

if !need_abort
    && let Some(budget) = time_budget
    && all_matches.len() > 1
    && search_start.elapsed() > budget
{
    need_abort = true;
}

For a zero-match search, all_matches.len() remains zero, so the deadline condition is never applied.

Removing the match-count condition, while preserving cursor accounting for the partially consumed chunk, appears necessary.

Relationship to #746 / #750

This is related to, but not resolved by:

PR #750 added timeBudgetMs to the pi-fff wrapper and deduplicated auxiliary index creation. It did not address the native zero-match budget bypass demonstrated here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriaged

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions