Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#3673: add retry and random bricks #3674

Merged
merged 3 commits into from
Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/blocks/transformers/controlFlow/Retry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2022 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import blockRegistry from "@/blocks/registry";
import {
echoBlock,
simpleInput,
testOptions,
throwBlock,
} from "@/runtime/pipelineTests/pipelineTestHelpers";
import { reducePipeline } from "@/runtime/reducePipeline";
import * as logging from "@/background/messenger/api";
import { makePipelineExpression } from "@/testUtils/expressionTestHelpers";
import Retry from "@/blocks/transformers/controlFlow/Retry";

(logging.getLoggingConfig as any) = jest.fn().mockResolvedValue({
logValues: true,
});

const retryBlock = new Retry();

beforeEach(() => {
blockRegistry.clear();
blockRegistry.register(throwBlock, echoBlock, retryBlock);
});

describe("Retry", () => {
test("throws error if retries fail", async () => {
const pipeline = {
id: retryBlock.id,
config: {
maxRetries: 2,
body: makePipelineExpression([
{
id: throwBlock.id,
config: {
message: "This is an error message!",
},
},
]),
},
};

return expect(
reducePipeline(pipeline, simpleInput({}), testOptions("v3"))
).rejects.toThrow();
});

test("returns result on success", async () => {
const pipeline = {
id: retryBlock.id,
config: {
maxRetries: 2,
body: makePipelineExpression([
{
id: echoBlock.id,
config: {
message: "Hello, world!",
},
},
]),
},
};

const result = await reducePipeline(
pipeline,
simpleInput({}),
testOptions("v3")
);

expect(result).toStrictEqual({
message: "Hello, world!",
});
});
});
104 changes: 104 additions & 0 deletions src/blocks/transformers/controlFlow/Retry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright (C) 2022 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { Transformer } from "@/types";
import { BlockArg, BlockOptions, Schema } from "@/core";
import { propertiesToSchema } from "@/validators/generic";
import { PipelineExpression } from "@/runtime/mapArgs";
import { validateRegistryId } from "@/types/helpers";
import { sleep } from "@/utils";
import { BusinessError } from "@/errors/businessErrors";

class Retry extends Transformer {
static BLOCK_ID = validateRegistryId("@pixiebrix/retry");
defaultOutputKey = "retryOutput";

constructor() {
super(Retry.BLOCK_ID, "Retry", "Retry bricks on error");
BLoe marked this conversation as resolved.
Show resolved Hide resolved
}

override async isPure(): Promise<boolean> {
// Safe default -- need to be able to inspect the inputs to determine if pure
return false;
}

override async isRootAware(): Promise<boolean> {
// Safe default -- need to be able to inspect the inputs to determine if any sub-calls are root aware
return true;
}

inputSchema: Schema = propertiesToSchema(
{
body: {
$ref: "https://app.pixiebrix.com/schemas/pipeline#",
description: "The bricks to execute",
},
intervalMillis: {
type: "number",
description: "Number of milliseconds to wait between retries",
},
maxRetries: {
type: "number",
description:
"The maximum number of retries (not including the initial run)",
default: 3,
},
},
["body"]
);

async transform(
{
body: bodyPipeline,
maxRetries = Number.MAX_SAFE_INTEGER,
intervalMillis,
}: BlockArg<{
body: PipelineExpression;
intervalMillis?: number;
maxRetries?: number;
}>,
options: BlockOptions
): Promise<unknown> {
let lastError: unknown;
let retryCount = 0;

while (retryCount < maxRetries) {
if (retryCount > 0) {
// eslint-disable-next-line no-await-in-loop -- retry loop
await sleep(intervalMillis);
}

try {
// eslint-disable-next-line no-await-in-loop -- retry loop
return await options.runPipeline(bodyPipeline.__value__);
} catch (error) {
lastError = error;
}

retryCount++;
}

if (!lastError) {
// In practice, lastError will always be set. But throw just in case
throw new BusinessError("Maximum number of retries exceeded");
}

throw lastError;
}
}

export default Retry;
36 changes: 36 additions & 0 deletions src/blocks/transformers/randomNumber.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2022 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { unsafeAssumeValidArg } from "@/runtime/runtimeTypes";
import { RandomNumber } from "@/blocks/transformers/randomNumber";

describe("random number", () => {
it("returns a random integer", async () => {
const { value } = await new RandomNumber().transform(
unsafeAssumeValidArg({ lower: 0, upper: 5 })
);
expect(value).toBeInteger();
});

it("returns a random float", async () => {
const { value } = await new RandomNumber().transform(
unsafeAssumeValidArg({ lower: 0, upper: 5, floating: true })
);
expect(value).not.toBeInteger();
expect(value).toBeNumber();
});
});
76 changes: 76 additions & 0 deletions src/blocks/transformers/randomNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2022 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { Transformer } from "@/types";
import { propertiesToSchema } from "@/validators/generic";
import { BlockArg } from "@/core";
import { random } from "lodash";

export class RandomNumber extends Transformer {
constructor() {
super(
"@pixiebrix/random",
"Random Number",
"Generate a random number",
"faCode"
);
}

override async isPure(): Promise<boolean> {
return false;
}

inputSchema = propertiesToSchema(
{
lower: {
type: "number",
description: "The lower bound",
default: 0,
},
upper: {
type: "number",
description: "The upper bound",
default: 1,
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to specify inclusive/exclusive in the descriptions here.

floating: {
type: "boolean",
description: "Specify returning a floating-point number.",
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could maybe be a little more clear to specify integer or float in the description.

},
[]
);

override outputSchema = propertiesToSchema({
value: {
type: "number",
},
});

async transform({
lower = 0,
upper = 1,
floating = false,
}: BlockArg<{
lower?: number;
upper?: number;
floating?: boolean;
}>): Promise<{ value: number }> {
return {
value: random(lower, upper, floating),
};
}
}
4 changes: 4 additions & 0 deletions src/blocks/transformers/registerTransformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import ForEach from "./controlFlow/ForEach";
import IfElse from "./controlFlow/IfElse";
import TryExcept from "./controlFlow/TryExcept";
import ForEachElement from "@/blocks/transformers/controlFlow/ForEachElement";
import { RandomNumber } from "@/blocks/transformers/randomNumber";
import Retry from "@/blocks/transformers/controlFlow/Retry";

function registerTransformers() {
registerBlock(new JQTransformer());
Expand Down Expand Up @@ -71,12 +73,14 @@ function registerTransformers() {
registerBlock(new ParseDataUrl());
registerBlock(new ParseDate());
registerBlock(new ScreenshotTab());
registerBlock(new RandomNumber());

// Control Flow Bricks
registerBlock(new ForEach());
registerBlock(new IfElse());
registerBlock(new TryExcept());
registerBlock(new ForEachElement());
registerBlock(new Retry());
}

export default registerTransformers;
2 changes: 1 addition & 1 deletion src/development/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import fs from "fs";
import blockRegistry from "@/blocks/registry";

// Maintaining this number is a simple way to ensure bricks don't accidentally get dropped
const EXPECTED_HEADER_COUNT = 102;
const EXPECTED_HEADER_COUNT = 104;

// Import for side-effects (these modules register the blocks)
// NOTE: we don't need to also include extensionPoints because we got rid of all the legacy hard-coded extension points
Expand Down
5 changes: 5 additions & 0 deletions src/pageEditor/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from "@/components/documentBuilder/documentBuilderTypes";
import { joinElementName } from "@/components/documentBuilder/utils";
import ForEachElement from "@/blocks/transformers/controlFlow/ForEachElement";
import Retry from "@/blocks/transformers/controlFlow/Retry";

export async function getCurrentURL(): Promise<string> {
if (!browser.devtools) {
Expand Down Expand Up @@ -68,6 +69,10 @@ export function getPipelinePropNames(block: BlockConfig): string[] {
return ["body"];
}

case Retry.BLOCK_ID: {
return ["body"];
}

case ForEachElement.BLOCK_ID: {
return ["body"];
}
Expand Down