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

feat(orchestrator): implement task processor #2221

Merged
merged 3 commits into from
May 30, 2024

Conversation

TBonnin
Copy link
Collaborator

@TBonnin TBonnin commented May 29, 2024

Describe your changes

This PR is implementing the orchestrator processor which, for now, is being run by jobs.
The processor is behind a flag flag:orchestrator:dryrun:process:global. It is still in dryrun mode since no real processing is happening, tasks are either immediately succeeded or failed. It is the last step before actually processing webhook/actions

Notes

  • I have added orchestrator endpoints to support dequeueing, set task state, heartbeat and their respective functions in the orchestrator client
  • A processor dequeues tasks in a infinite loop and process the task via the process functions passed as argument.
  • A processor also check if pending tasks have been completed (ie: they might have expired or be cancelled) and send abort signal.
  • In jobs, 2 processor worker_thread are being started. One for action and one for webhook to reproduce what we have right now with webhooks being in a separate temporal queue

Tested in staging

Checklist before requesting a review (skip if just adding/editing APIs & templates)

  • I added tests, otherwise the reason is:
  • I added observability, otherwise the reason is:
  • I added analytics, otherwise the reason is:

@TBonnin TBonnin force-pushed the tbonnin/orchestrator-processor branch from 853da7a to 8a1e376 Compare May 29, 2024 18:47
payload: { taskId }
});
} else {
return Ok(undefined);
Copy link
Member

Choose a reason for hiding this comment

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

should this be null?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

null is not assignable to type void. Only undefined is. 🤷

@TBonnin TBonnin force-pushed the tbonnin/orchestrator-processor branch from 8a1e376 to d4d3bd4 Compare May 29, 2024 19:11
@khaliqgant khaliqgant self-requested a review May 29, 2024 19:12
processor.stop();
}
}, 1000);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

using setInterval because of the flag so it can be enabled/disabled without restarting jobs. Once stable it will be a simple processor.start()

// TODO: Implement action processing
// Returning an error for now
return Err(`Not implemented: ${JSON.stringify({ taskId: task.id })}`);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

dry-run: not doing real processing yet, just marking action as successful and webhook as failed for testing purpse

import { validateTask } from './validate.js';
import type { JsonValue } from 'type-fest';

export class OrchestratorClient {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

everything until dequeue was already there. I have moved the file in a clients subdir

@@ -10,12 +10,9 @@ type Health = Endpoint<{
const path = '/health';
const method = 'GET';

export const handler: RouteHandler<Health> = {
export const getHandler: RouteHandler<Health> = {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since each route file corresponds to a path it is possible to have multiple Endpoint/handler per file, one for each HTTP method. So I renamed the Endpoint and handlers to make it clearer

Copy link
Collaborator

Choose a reason for hiding this comment

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

I tried to keep one per file by prefixing the filename, i.e: getHealth.ts would make sense to me and keep files small

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sensible. I will do the same then

import type { ApiError, Endpoint } from '@nangohq/types';
import type { EndpointRequest, EndpointResponse, RouteHandler, Route } from '@nangohq/utils';
import { validateRequest } from '@nangohq/utils';

Copy link
Member

Choose a reason for hiding this comment

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

What is the purpose of search in the context of the processor?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is the search task endpoint. The processor must be able to check if pending tasks have been cancelled so it need to be able to search for specific tasks.
This endpoint could also be used in internal tooling to display list of tasks per queue/state/etc...

@TBonnin TBonnin force-pushed the tbonnin/orchestrator-processor branch 2 times, most recently from d3591ab to e57716e Compare May 29, 2024 19:47
@bodinsamuel
Copy link
Collaborator

I'm late in my reviews, checking this in a couple of hours 👍🏻

Copy link
Collaborator

@bodinsamuel bodinsamuel left a comment

Choose a reason for hiding this comment

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

Very nice progress! That's a lot of code and complexity to review, hopefully we'll get used to it quickly 💯

packages/orchestrator/lib/clients/processor.ts Outdated Show resolved Hide resolved
packages/orchestrator/lib/clients/processor.ts Outdated Show resolved Hide resolved
payload: webhookArgsSchema
});

export function validateTask(task: Task): Result<TaskAction | TaskWebhook> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I feel like Task{ type: string } would have been more scalable, because we'll need to add syncs and then maybe more stuff along the way

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

they don't have the same fields though. Having some type guard functionality is nice though even though I agree that the definitions are more complex

Copy link
Collaborator

Choose a reason for hiding this comment

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

I mean you can still validate the payload but it's just a waste of time to not use a discriminator 👍🏻

@@ -10,12 +10,9 @@ type Health = Endpoint<{
const path = '/health';
const method = 'GET';

export const handler: RouteHandler<Health> = {
export const getHandler: RouteHandler<Health> = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I tried to keep one per file by prefixing the filename, i.e: getHealth.ts would make sense to me and keep files small

packages/orchestrator/lib/routes/v1/search.ts Outdated Show resolved Hide resolved
cleanupAndRespond((res) => res.status(200).send([]));
}, waitForCompletionTimeoutMs);

eventEmitter.once(eventId, onTaskStarted);
Copy link
Collaborator

Choose a reason for hiding this comment

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

not sure I get the wait for completion in that case, because if don't receive an answer you'll just wait 60s for nothing

Copy link
Collaborator Author

@TBonnin TBonnin May 30, 2024

Choose a reason for hiding this comment

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

you are waiting for a task to be created/dequeuable, so you don't have to keep making request. Trying hard not to have a push based system that would requires handling states but in the scenario of a processor it could be an acceptable solution.

Copy link
Collaborator

Choose a reason for hiding this comment

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

okay took me a while to understand the executeAction -> execute -> schedule -> immediate -> event
that works 👍🏻

packages/orchestrator/lib/clients/processor.ts Outdated Show resolved Hide resolved
packages/jobs/lib/app.ts Show resolved Hide resolved
packages/jobs/lib/env.ts Show resolved Hide resolved
@TBonnin TBonnin changed the title chore(orchestrator): implement task processor feat(orchestrator): implement task processor May 30, 2024
if (this.queue.available() > 0) {
const tasks = await this.orchestratorClient.dequeue({ groupKey: this.groupKey, limit: this.queue.available() * 2, waitForCompletion: true }); // fetch more than available to keep the queue full
if (tasks.isErr()) {
logger.error(`failed to dequeue tasks: ${stringifyError(tasks.error)}`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

just a heads up, happened to me once when the API stop responding all fetch are failing fast ultimately using the whole cpu. Might be an edge case but fyi

Copy link
Collaborator Author

@TBonnin TBonnin May 30, 2024

Choose a reason for hiding this comment

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

good point. introducing a little delay in case of error

Copy link
Collaborator

@bodinsamuel bodinsamuel left a comment

Choose a reason for hiding this comment

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

🛫

@TBonnin TBonnin force-pushed the tbonnin/orchestrator-processor branch from 0a8e96d to 8ac4bb9 Compare May 30, 2024 17:50
@TBonnin TBonnin force-pushed the tbonnin/orchestrator-processor branch from 8ac4bb9 to 3503468 Compare May 30, 2024 18:54
@TBonnin TBonnin enabled auto-merge (squash) May 30, 2024 18:54
@TBonnin TBonnin merged commit d1ac7a3 into master May 30, 2024
21 checks passed
@TBonnin TBonnin deleted the tbonnin/orchestrator-processor branch May 30, 2024 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants