Skip to content

Commit

Permalink
#3673: add retry and random bricks (#3674)
Browse files Browse the repository at this point in the history
  • Loading branch information
twschiller committed Jun 13, 2022
1 parent a0bf7b6 commit 76dac91
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 1 deletion.
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");
}

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();
});
});
82 changes: 82 additions & 0 deletions src/blocks/transformers/randomNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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";
import { BusinessError } from "@/errors/businessErrors";

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 (inclusive)",
default: 0,
},
upper: {
type: "number",
description: "The upper bound (exclusive)",
default: 1,
},
floating: {
type: "boolean",
description:
"Flag to return a decimal (floating point) number instead of an integer.",
},
},
[]
);

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

async transform({
lower = 0,
upper = 1,
floating = false,
}: BlockArg<{
lower?: number;
upper?: number;
floating?: boolean;
}>): Promise<{ value: number }> {
if (lower > upper) {
throw new BusinessError("lower bound cannot be greater than upper bound");
}

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

0 comments on commit 76dac91

Please sign in to comment.