mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat(coderd): add new dispatch logic for coder inbox (#16764)
This PR is [resolving the dispatch part of Coder Inbocx](https://github.com/coder/internal/issues/403). Since the DB layer has been merged - we now want to insert notifications into Coder Inbox in parallel of the other delivery target. To do so, we push two messages instead of one using the `Enqueue` method.
This commit is contained in:
@ -53,13 +53,13 @@ func NewStoreEnqueuer(cfg codersdk.NotificationsConfig, store Store, helpers tem
|
||||
}
|
||||
|
||||
// Enqueue queues a notification message for later delivery, assumes no structured input data.
|
||||
func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) {
|
||||
func (s *StoreEnqueuer) Enqueue(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, createdBy string, targets ...uuid.UUID) ([]uuid.UUID, error) {
|
||||
return s.EnqueueWithData(ctx, userID, templateID, labels, nil, createdBy, targets...)
|
||||
}
|
||||
|
||||
// Enqueue queues a notification message for later delivery.
|
||||
// Messages will be dequeued by a notifier later and dispatched.
|
||||
func (s *StoreEnqueuer) EnqueueWithData(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) (*uuid.UUID, error) {
|
||||
func (s *StoreEnqueuer) EnqueueWithData(ctx context.Context, userID, templateID uuid.UUID, labels map[string]string, data map[string]any, createdBy string, targets ...uuid.UUID) ([]uuid.UUID, error) {
|
||||
metadata, err := s.store.FetchNewMessageMetadata(ctx, database.FetchNewMessageMetadataParams{
|
||||
UserID: userID,
|
||||
NotificationTemplateID: templateID,
|
||||
@ -85,40 +85,48 @@ func (s *StoreEnqueuer) EnqueueWithData(ctx context.Context, userID, templateID
|
||||
return nil, xerrors.Errorf("failed encoding input labels: %w", err)
|
||||
}
|
||||
|
||||
id := uuid.New()
|
||||
err = s.store.EnqueueNotificationMessage(ctx, database.EnqueueNotificationMessageParams{
|
||||
ID: id,
|
||||
UserID: userID,
|
||||
NotificationTemplateID: templateID,
|
||||
Method: dispatchMethod,
|
||||
Payload: input,
|
||||
Targets: targets,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: dbtime.Time(s.clock.Now().UTC()),
|
||||
})
|
||||
if err != nil {
|
||||
// We have a trigger on the notification_messages table named `inhibit_enqueue_if_disabled` which prevents messages
|
||||
// from being enqueued if the user has disabled them via notification_preferences. The trigger will fail the insertion
|
||||
// with the message "cannot enqueue message: user has disabled this notification".
|
||||
//
|
||||
// This is more efficient than fetching the user's preferences for each enqueue, and centralizes the business logic.
|
||||
if strings.Contains(err.Error(), ErrCannotEnqueueDisabledNotification.Error()) {
|
||||
return nil, ErrCannotEnqueueDisabledNotification
|
||||
uuids := make([]uuid.UUID, 0, 2)
|
||||
// All the enqueued messages are enqueued both on the dispatch method set by the user (or default one) and the inbox.
|
||||
// As the inbox is not configurable per the user and is always enabled, we always enqueue the message on the inbox.
|
||||
// The logic is done here in order to have two completely separated processing and retries are handled separately.
|
||||
for _, method := range []database.NotificationMethod{dispatchMethod, database.NotificationMethodInbox} {
|
||||
id := uuid.New()
|
||||
err = s.store.EnqueueNotificationMessage(ctx, database.EnqueueNotificationMessageParams{
|
||||
ID: id,
|
||||
UserID: userID,
|
||||
NotificationTemplateID: templateID,
|
||||
Method: method,
|
||||
Payload: input,
|
||||
Targets: targets,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: dbtime.Time(s.clock.Now().UTC()),
|
||||
})
|
||||
if err != nil {
|
||||
// We have a trigger on the notification_messages table named `inhibit_enqueue_if_disabled` which prevents messages
|
||||
// from being enqueued if the user has disabled them via notification_preferences. The trigger will fail the insertion
|
||||
// with the message "cannot enqueue message: user has disabled this notification".
|
||||
//
|
||||
// This is more efficient than fetching the user's preferences for each enqueue, and centralizes the business logic.
|
||||
if strings.Contains(err.Error(), ErrCannotEnqueueDisabledNotification.Error()) {
|
||||
return nil, ErrCannotEnqueueDisabledNotification
|
||||
}
|
||||
|
||||
// If the enqueue fails due to a dedupe hash conflict, this means that a notification has already been enqueued
|
||||
// today with identical properties. It's far simpler to prevent duplicate sends in this central manner, rather than
|
||||
// having each notification enqueue handle its own logic.
|
||||
if database.IsUniqueViolation(err, database.UniqueNotificationMessagesDedupeHashIndex) {
|
||||
return nil, ErrDuplicate
|
||||
}
|
||||
|
||||
s.log.Warn(ctx, "failed to enqueue notification", slog.F("template_id", templateID), slog.F("input", input), slog.Error(err))
|
||||
return nil, xerrors.Errorf("enqueue notification: %w", err)
|
||||
}
|
||||
|
||||
// If the enqueue fails due to a dedupe hash conflict, this means that a notification has already been enqueued
|
||||
// today with identical properties. It's far simpler to prevent duplicate sends in this central manner, rather than
|
||||
// having each notification enqueue handle its own logic.
|
||||
if database.IsUniqueViolation(err, database.UniqueNotificationMessagesDedupeHashIndex) {
|
||||
return nil, ErrDuplicate
|
||||
}
|
||||
|
||||
s.log.Warn(ctx, "failed to enqueue notification", slog.F("template_id", templateID), slog.F("input", input), slog.Error(err))
|
||||
return nil, xerrors.Errorf("enqueue notification: %w", err)
|
||||
uuids = append(uuids, id)
|
||||
}
|
||||
|
||||
s.log.Debug(ctx, "enqueued notification", slog.F("msg_id", id))
|
||||
return &id, nil
|
||||
s.log.Debug(ctx, "enqueued notification", slog.F("msg_ids", uuids))
|
||||
return uuids, nil
|
||||
}
|
||||
|
||||
// buildPayload creates the payload that the notification will for variable substitution and/or routing.
|
||||
@ -165,12 +173,12 @@ func NewNoopEnqueuer() *NoopEnqueuer {
|
||||
return &NoopEnqueuer{}
|
||||
}
|
||||
|
||||
func (*NoopEnqueuer) Enqueue(context.Context, uuid.UUID, uuid.UUID, map[string]string, string, ...uuid.UUID) (*uuid.UUID, error) {
|
||||
func (*NoopEnqueuer) Enqueue(context.Context, uuid.UUID, uuid.UUID, map[string]string, string, ...uuid.UUID) ([]uuid.UUID, error) {
|
||||
// nolint:nilnil // irrelevant.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (*NoopEnqueuer) EnqueueWithData(context.Context, uuid.UUID, uuid.UUID, map[string]string, map[string]any, string, ...uuid.UUID) (*uuid.UUID, error) {
|
||||
func (*NoopEnqueuer) EnqueueWithData(context.Context, uuid.UUID, uuid.UUID, map[string]string, map[string]any, string, ...uuid.UUID) ([]uuid.UUID, error) {
|
||||
// nolint:nilnil // irrelevant.
|
||||
return nil, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user