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:
Vincent Vielle
2025-03-05 22:43:18 +01:00
committed by GitHub
parent 32450a2f77
commit 522181fead
42 changed files with 415 additions and 120 deletions
+25 -15
View File
@@ -67,7 +67,8 @@ func TestMetrics(t *testing.T) {
})
handler := &fakeHandler{}
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
method: handler,
method: handler,
database.NotificationMethodInbox: &fakeHandler{},
})
enq, err := notifications.NewStoreEnqueuer(cfg, store, defaultHelpers(), logger.Named("enqueuer"), quartz.NewReal())
@@ -77,7 +78,10 @@ func TestMetrics(t *testing.T) {
// Build fingerprints for the two different series we expect.
methodTemplateFP := fingerprintLabels(notifications.LabelMethod, string(method), notifications.LabelTemplateID, tmpl.String())
methodTemplateFPWithInbox := fingerprintLabels(notifications.LabelMethod, string(database.NotificationMethodInbox), notifications.LabelTemplateID, tmpl.String())
methodFP := fingerprintLabels(notifications.LabelMethod, string(method))
methodFPWithInbox := fingerprintLabels(notifications.LabelMethod, string(database.NotificationMethodInbox))
expected := map[string]func(metric *dto.Metric, series string) bool{
"coderd_notifications_dispatch_attempts_total": func(metric *dto.Metric, series string) bool {
@@ -91,7 +95,8 @@ func TestMetrics(t *testing.T) {
var match string
for result, val := range results {
seriesFP := fingerprintLabels(notifications.LabelMethod, string(method), notifications.LabelTemplateID, tmpl.String(), notifications.LabelResult, result)
if !hasMatchingFingerprint(metric, seriesFP) {
seriesFPWithInbox := fingerprintLabels(notifications.LabelMethod, string(database.NotificationMethodInbox), notifications.LabelTemplateID, tmpl.String(), notifications.LabelResult, result)
if !hasMatchingFingerprint(metric, seriesFP) && !hasMatchingFingerprint(metric, seriesFPWithInbox) {
continue
}
@@ -115,7 +120,7 @@ func TestMetrics(t *testing.T) {
return metric.Counter.GetValue() == target
},
"coderd_notifications_retry_count": func(metric *dto.Metric, series string) bool {
assert.Truef(t, hasMatchingFingerprint(metric, methodTemplateFP), "found unexpected series %q", series)
assert.Truef(t, hasMatchingFingerprint(metric, methodTemplateFP) || hasMatchingFingerprint(metric, methodTemplateFPWithInbox), "found unexpected series %q", series)
if debug {
t.Logf("coderd_notifications_retry_count == %v: %v", maxAttempts-1, metric.Counter.GetValue())
@@ -125,7 +130,7 @@ func TestMetrics(t *testing.T) {
return metric.Counter.GetValue() == maxAttempts-1
},
"coderd_notifications_queued_seconds": func(metric *dto.Metric, series string) bool {
assert.Truef(t, hasMatchingFingerprint(metric, methodFP), "found unexpected series %q", series)
assert.Truef(t, hasMatchingFingerprint(metric, methodFP) || hasMatchingFingerprint(metric, methodFPWithInbox), "found unexpected series %q", series)
if debug {
t.Logf("coderd_notifications_queued_seconds > 0: %v", metric.Histogram.GetSampleSum())
@@ -140,7 +145,7 @@ func TestMetrics(t *testing.T) {
return metric.Histogram.GetSampleSum() > 0
},
"coderd_notifications_dispatcher_send_seconds": func(metric *dto.Metric, series string) bool {
assert.Truef(t, hasMatchingFingerprint(metric, methodFP), "found unexpected series %q", series)
assert.Truef(t, hasMatchingFingerprint(metric, methodFP) || hasMatchingFingerprint(metric, methodFPWithInbox), "found unexpected series %q", series)
if debug {
t.Logf("coderd_notifications_dispatcher_send_seconds > 0: %v", metric.Histogram.GetSampleSum())
@@ -170,7 +175,7 @@ func TestMetrics(t *testing.T) {
}
// 1 message will exceed its maxAttempts, 1 will succeed on the first try.
return metric.Counter.GetValue() == maxAttempts+1
return metric.Counter.GetValue() == (maxAttempts+1)*2 // *2 because we have 2 enqueuers.
},
}
@@ -252,8 +257,11 @@ func TestPendingUpdatesMetric(t *testing.T) {
assert.NoError(t, mgr.Stop(ctx))
})
handler := &fakeHandler{}
inboxHandler := &fakeHandler{}
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
method: handler,
method: handler,
database.NotificationMethodInbox: inboxHandler,
})
enq, err := notifications.NewStoreEnqueuer(cfg, store, defaultHelpers(), logger.Named("enqueuer"), quartz.NewReal())
@@ -285,7 +293,7 @@ func TestPendingUpdatesMetric(t *testing.T) {
}()
// Both handler calls should be pending in the metrics.
require.EqualValues(t, 2, promtest.ToFloat64(metrics.PendingUpdates))
require.EqualValues(t, 4, promtest.ToFloat64(metrics.PendingUpdates))
// THEN:
// Trigger syncing updates
@@ -293,13 +301,13 @@ func TestPendingUpdatesMetric(t *testing.T) {
// Wait until we intercept the calls to sync the pending updates to the store.
success := testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, interceptor.updateSuccess)
require.EqualValues(t, 1, success)
require.EqualValues(t, 2, success)
failure := testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, interceptor.updateFailure)
require.EqualValues(t, 1, failure)
require.EqualValues(t, 2, failure)
// Validate that the store synced the expected number of updates.
require.Eventually(t, func() bool {
return syncer.sent.Load() == 1 && syncer.failed.Load() == 1
return syncer.sent.Load() == 2 && syncer.failed.Load() == 2
}, testutil.WaitShort, testutil.IntervalFast)
// Wait for the updates to be synced and the metric to reflect that.
@@ -342,7 +350,8 @@ func TestInflightDispatchesMetric(t *testing.T) {
// Barrier handler will wait until all notification messages are in-flight.
barrier := newBarrierHandler(msgCount, handler)
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
method: barrier,
method: barrier,
database.NotificationMethodInbox: &fakeHandler{},
})
enq, err := notifications.NewStoreEnqueuer(cfg, store, defaultHelpers(), logger.Named("enqueuer"), quartz.NewReal())
@@ -378,7 +387,7 @@ func TestInflightDispatchesMetric(t *testing.T) {
// Wait for the updates to be synced and the metric to reflect that.
require.Eventually(t, func() bool {
return promtest.ToFloat64(metrics.InflightDispatches) == 0
return promtest.ToFloat64(metrics.InflightDispatches.WithLabelValues(string(method), tmpl.String())) == 0
}, testutil.WaitShort, testutil.IntervalFast)
}
@@ -427,8 +436,9 @@ func TestCustomMethodMetricCollection(t *testing.T) {
smtpHandler := &fakeHandler{}
webhookHandler := &fakeHandler{}
mgr.WithHandlers(map[database.NotificationMethod]notifications.Handler{
defaultMethod: smtpHandler,
customMethod: webhookHandler,
defaultMethod: smtpHandler,
customMethod: webhookHandler,
database.NotificationMethodInbox: &fakeHandler{},
})
enq, err := notifications.NewStoreEnqueuer(cfg, store, defaultHelpers(), logger.Named("enqueuer"), quartz.NewReal())