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(api): refactor of trigger event #2825

Merged
merged 2 commits into from
Feb 20, 2023

Conversation

p-fernandez
Copy link
Contributor

What change does this PR introduce?

Refactor of trigger event.
Also moves ISubscribersDefine interface to @novu/shared as it was being exported from @novu/node. Felt weird our API had that dependency from a client package.

Why was this change needed?

Trigger event calls ProcessSubscriber that inside was hosting the creation of the notification and the building of the job steps for the jobs of the notification. Felt like a confusing implementation so decided to isolate both use cases into ProcessSubscriber and CreateNotification.

Other information (Screenshots)

@linear
Copy link

linear bot commented Feb 17, 2023

NV-1724 Feature: refactor trigger event

Why? (Context)

Trigger event calls ProcessSubscriber that inside was hosting the creation of the notification and the building of the job steps for the jobs of the notification. Felt like a confusing implementation so decided to isolate both use cases into ProcessSubscriber and CreateNotification.

What?

Refactor.

Definition of Done

We have a clear separation of logic for the subscriber domain operations and the notification domain operations.

The code is easier to understand and any developer can understand the flow of what's happening inside of the trigger of an event.

Trigger event is left unmodified functionality wise.

Existing test suites pass.

@@ -5,11 +5,12 @@ import {
NotificationTemplateEntity,
SubscriberEntity,
} from '@novu/dal';
import { ISubscribersDefine, ITopic, TriggerRecipients } from '@novu/node';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Didn't make sense to me an interface that is shared in the monorepo to be exported from the Node.js client package.

overrides: Record<string, Record<string, unknown>>;

@IsDefined()
payload: any; // eslint-disable-line @typescript-eslint/no-explicit-any
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think here we would need to also make shareable the interface ITriggerPayload so we can remove all any set for payload property. But not 100% sure if it will be right interface.

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, I saw this couple of times when we use any and add the eslint-disable comment, I agree we should have the type for the payload and we might assign it to Record<string, unknown> to get rid of the eslint comment...

type ITriggerPayload = Record<string, unknown>;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Soon to be fixed.

);

const jobs: Omit<JobEntity, '_id'>[] = [];
public async execute(command: ProcessSubscriberCommand): Promise<SubscriberEntity | undefined> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now this use case does just one thing, process the subscriber.

Comment on lines +47 to +48
const template = await this.notificationTemplateRepository.findByTriggerIdentifier(
command.environmentId,
command.identifier
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Potentially we could send the template from the queued data as during ParseEventRequest we already check the template exists, but I find sensible to retrieve it again in case during the queued time for the event, the template is being somehow deleted.

Comment on lines +56 to +58
if (!template) {
const message = 'Notification template could not be found';
Logger.error(message, LOG_CONTEXT);
throw new ApiException(message);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Saving execution time and DB calls if we reach this scenario.

Copy link
Contributor

Choose a reason for hiding this comment

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

The use-case TriggerEvent is used by the TriggerHandlerQueueService and throwing that error won't work for the case when the trigger queue events are processed, because there is no REST context anymore... so I feel like it should throw some generic runtime error and we should catch and wrap it in other places... wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not totally understand the point you explain. ApiException is a generic runtime error. We already throw something similar in the method validateTransactionIdProperty that validates the entry data. And the worker at the end it has its own context, though mostly of the information provided will come from the database. I am happy to discuss this privately if it is easier because I couldn't understand what you mean, sorry. 😓

Copy link
Contributor

Choose a reason for hiding this comment

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

APIException extends BadRequestException which is HttpException, but we are not in that HTTP context anymore, so I think that this is not an appropriate exception here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤦🏻 We have a bigger problem then. We would need to change the definition of ApiException and the use in the monorepo. The are more places where it is used under the queue context. Adding to technical debt.

Comment on lines +65 to +73
let actorProcessed;
if (actor) {
actorProcessed = await this.processSubscriber.execute(
ProcessSubscriberCommand.create({
environmentId,
organizationId,
userId,
subscriber: actor,
})
);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before, when sending the actor, we were searching for it in the DB inside the loop when executing ProcessSubscriber. Therefore we would be searching for the actor number of subscribers times. Not efficient. 😅
Also its processing is the same as any subscriber recipient, so that helped to simplify ProcessSubscriber code.

);

// If no subscriber makes no sense to try to create notification
if (subscriberProcessed) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment says it all. Before we would try to create a notification even if the subscriber didn't exist in the database.

Copy link
Contributor

Choose a reason for hiding this comment

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

should we then return even before we enter this for loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean instead to do:

if (!subscriberProcessed) {
  continue;
}

?
RIght now I am surrounding the loop code to execute with the condition so nothing is executed if no subscriber. And the query we have to do it, to double check in case data stored in the queue and database data is not consistent.

Copy link
Contributor

Choose a reason for hiding this comment

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

nvm, I feel like I didn't notice that the code is surrounded within the same if...

Comment on lines -129 to -131
@Cached(CacheKeyPrefixEnum.NOTIFICATION_TEMPLATE)
private async getNotificationTemplate({ _id, environmentId }: { _id: string; environmentId: string }) {
return await this.notificationTemplateRepository.findById(_id, environmentId);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't move this code at all and removed it because we are passing the template all the time to the notification creation so not needed to cache its retrieval as we only need to look for it once.
Before it made sense as we were checking in the loop for every subscriber if the template existed.
CC: @djabarovgeorge

@p-fernandez p-fernandez force-pushed the nv-1724-feature-refactor-trigger-event branch 2 times, most recently from 478f534 to d5c03b8 Compare February 17, 2023 11:20
Copy link
Contributor

@LetItRock LetItRock left a comment

Choose a reason for hiding this comment

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

awesome! 🚀 left some small suggestions

overrides: Record<string, Record<string, unknown>>;

@IsDefined()
payload: any; // eslint-disable-line @typescript-eslint/no-explicit-any
Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, I saw this couple of times when we use any and add the eslint-disable comment, I agree we should have the type for the payload and we might assign it to Record<string, unknown> to get rid of the eslint comment...

type ITriggerPayload = Record<string, unknown>;

type NotificationJob = Omit<JobEntity, '_id'>;

@Injectable()
export class CreateNotification {
Copy link
Contributor

Choose a reason for hiding this comment

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

this one should be called probably CreateNotificationJobs because it's what it returns

subscriberPayload,
subscriber: SubscriberEntity | null
) {
// TODO: Getting rid of this null would be amazing
Copy link
Contributor

Choose a reason for hiding this comment

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

after we do a findBySubscriberId we should have a check and throw if null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we throw we would break the loop. That would mean that some notifications will be processed from the trigger but not all, leading to inconsistency. Unless we set up a rollback for the full trigger, for now I think it is better to leave the current behaviour of sending to the existing subscribers the notifications, and just skip it for non existent subscribers or failed subscribers.
Something worth to bring to tech summit to see what's the best direction we want to have for best fault tolerance. 👍🏻

Copy link
Contributor

Choose a reason for hiding this comment

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

ahhh ok sorry I was confused about this TODO comment, I think it's not possible to get rid of that null type in this function then because the subscriber is created in the createSubscriberUsecase.execute call when it's null...

Comment on lines +56 to +58
if (!template) {
const message = 'Notification template could not be found';
Logger.error(message, LOG_CONTEXT);
throw new ApiException(message);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The use-case TriggerEvent is used by the TriggerHandlerQueueService and throwing that error won't work for the case when the trigger queue events are processed, because there is no REST context anymore... so I feel like it should throw some generic runtime error and we should catch and wrap it in other places... wdyt?

);

// If no subscriber makes no sense to try to create notification
if (subscriberProcessed) {
Copy link
Contributor

Choose a reason for hiding this comment

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

should we then return even before we enter this for loop?

@p-fernandez p-fernandez force-pushed the nv-1724-feature-refactor-trigger-event branch from 6a0e315 to 3fd3df4 Compare February 20, 2023 09:52
@p-fernandez p-fernandez force-pushed the nv-1724-feature-refactor-trigger-event branch from ba733bc to 228b08c Compare February 20, 2023 14:10
@p-fernandez p-fernandez added this pull request to the merge queue Feb 20, 2023
Merged via the queue into next with commit 72319a1 Feb 20, 2023
@p-fernandez p-fernandez deleted the nv-1724-feature-refactor-trigger-event branch February 20, 2023 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants