npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@convex-dev/action-retrier

v0.1.3

Published

Convex component for retrying idempotent actions.

Downloads

126

Readme

Convex Action Retrier

npm version

Actions can sometimes fail due to network errors, server restarts, or issues with a 3rd party API, and it's often useful to retry them. The Action Retrier component makes this really easy.

import { ActionRetrier } from "@convex-dev/action-retrier";
import { components } from "./convex/_generated/server";

const retrier = new ActionRetrier(components.actionRetrier);

// `retrier.run` will automatically retry your action up to four times before giving up.
await retrier.run(ctx, internal.module.myAction, { arg: 123 });

Then, the retrier component will run the action and retry it on failure, sleeping with exponential backoff, until the action succeeds or the maximum number of retries is reached.

Installation

First, add @convex-dev/action-retrier as an NPM dependency:

npm install @convex-dev/action-retrier

Then, install the component into your Convex project within the convex/convex.config.ts configuration file:

// convex/convex.config.ts
import { defineApp } from "convex/server";
import actionRetrier from "@convex-dev/action-retrier/convex.config.js";

const app = defineApp();
app.use(actionRetrier);
export default app;

Finally, create a new ActionRetrier within your Convex project, and point it to the installed component:

// convex/index.ts
import { ActionRetrier } from "@convex-dev/action-retrier";
import { components } from "./_generated/api";

export const retrier = new ActionRetrier(components.actionRetrier);

You can optionally configure the retrier's backoff behavior in the ActionRetrier constructor.

const retrier = new ActionRetrier(components.actionRetrier, {
  initialBackoffMs: 10000,
  base: 10,
  maxFailures: 4,
});
  • initialBackoffMs is the initial delay after a failure before retrying (default: 250).
  • base is the base for the exponential backoff (default: 2).
  • maxFailures is the maximum number of times to retry the action (default: 4).

API

Starting a run

After installing the component, use the run method from either a mutation or action to kick off an action.

// convex/index.ts

export const exampleAction = internalAction({
  args: { failureRate: v.number() },
  handler: async (ctx, args) => {
    if (Math.random() < args.failureRate) {
      throw new Error("I can't go for that.");
    }
  },
});

export const kickoffExampleAction = action(async (ctx) => {
  const runId = await retrier.run(ctx, internal.index.exampleAction, {
    failureRate: 0.8,
  });
});

You can optionally specify overrides to the backoff parameters in an options argument.

// convex/index.ts

export const kickoffExampleAction = action(async (ctx) => {
  const runId = await retrier.run(
    ctx,
    internal.index.exampleAction,
    { failureRate: 0.8 },
    {
      initialBackoffMs: 125,
      base: 2.71,
      maxFailures: 3,
    },
  );
});

You can specify an onComplete mutation callback in the options argument as well. This mutation is guaranteed to eventually run exactly once.

// convex/index.ts

import { runResultValidator } from "@convex-dev/action-retrier";

export const kickoffExampleAction = action(async (ctx) => {
  const runId = await retrier.run(
    ctx,
    internal.index.exampleAction,
    { failureRate: 0.8 },
    {
      onComplete: internal.index.exampleCallback,
    },
  );
});

export const exampleCallback = internalMutation({
  args: { result: runResultValidator },
  handler: async (ctx, args) => {
    if (args.result.type === "success") {
      console.log(
        "Action succeeded with return value:",
        args.result.returnValue,
      );
    } else if (args.result.type === "failed") {
      console.log("Action failed with error:", args.result.error);
    } else if (args.result.type === "canceled") {
      console.log("Action was canceled.");
    }
  },
});

Run status

The run method returns a RunId, which can then be used for querying a run's status.

export const kickoffExampleAction = action(async (ctx) => {
  const runId = await retrier.run(ctx, internal.index.exampleAction, {
    failureRate: 0.8,
  });
  while (true) {
    const status = await retrier.status(ctx, runId);
    if (status.type === "inProgress") {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      continue;
    } else {
      console.log("Run completed with result:", status.result);
      break;
    }
  }
});

Canceling a run

You can cancel a run using the cancel method.

export const kickoffExampleAction = action(async (ctx) => {
  const runId = await retrier.run(ctx, internal.index.exampleAction, {
    failureRate: 0.8,
  });
  await new Promise((resolve) => setTimeout(resolve, 1000));
  await retrier.cancel(ctx, runId);
});

Runs that are currently executing will be canceled best effort, so they may still continue to execute. A succcesful call to cancel, however, does guarantee that subsequent status calls will indicate cancelation.

Cleaning up completed runs

Runs take up space in the database, since they store their return values. After a run completes, you can immediately clean up its storage by using retrier.cleanup(ctx, runId). The system will automatically cleanup completed runs after 7 days.

export const kickoffExampleAction = action(async (ctx) => {
  const runId = await retrier.run(ctx, internal.index.exampleAction, {
    failureRate: 0.8,
  });
  try {
    while (true) {
      const status = await retrier.status(ctx, runId);
      if (status.type === "inProgress") {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        continue;
      } else {
        console.log("Run completed with result:", status.result);
        break;
      }
    }
  } finally {
    await retrier.cleanup(ctx, runId);
  }
});

Logging

You can set the ACTION_RETRIER_LOG_LEVEL to DEBUG to have the retrier log out more of its internal information, which you can then view on the Convex dashboard.

npx convex env set ACTION_RETRIER_LOG_LEVEL DEBUG

The default log level is INFO, but you can also set it to ERROR for even fewer logs.

🧑‍🏫 What is Convex?

Convex is a hosted backend platform with a built-in database that lets you write your database schema and server functions in TypeScript. Server-side database queries automatically cache and subscribe to data, powering a realtime useQuery hook in our React client. There are also clients for Python, Rust, ReactNative, and Node, as well as a straightforward HTTP API.

The database supports NoSQL-style documents with opt-in schema validation, relationships and custom indexes (including on fields in nested objects).

The query and mutation server functions have transactional, low latency access to the database and leverage our v8 runtime with determinism guardrails to provide the strongest ACID guarantees on the market: immediate consistency, serializable isolation, and automatic conflict resolution via optimistic multi-version concurrency control (OCC / MVCC).

The action server functions have access to external APIs and enable other side-effects and non-determinism in either our optimized v8 runtime or a more flexible node runtime.

Functions can run in the background via scheduling and cron jobs.

Development is cloud-first, with hot reloads for server function editing via the CLI, preview deployments, logging and exception reporting integrations, There is a dashboard UI to browse and edit data, edit environment variables, view logs, run server functions, and more.

There are built-in features for reactive pagination, file storage, reactive text search, vector search, https endpoints (for webhooks), snapshot import/export, streaming import/export, and runtime validation for function arguments and database data.

Everything scales automatically, and it’s free to start.