Skip to content

Commit 8f78bad

Browse files
authored
feat(scaletest): switch notification trigger from creating a user to template deletion (coder#20512)
This PR refactors the notification scale test to use template admins and template deletion as the notification trigger. Additionally, I've added a configurable timeout for SMTP requests. Previously, notifications were triggered by creating/deleting a user, and notifications were received by users with the owner role. However, because of how many notifications were generated by the runners, we had too many notifications to reliably test notification delivery.
1 parent 0f8f67e commit 8f78bad

File tree

5 files changed

+91
-54
lines changed

5 files changed

+91
-54
lines changed

cli/exp_scaletest_notifications.go

Lines changed: 76 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package cli
44

55
import (
6+
"bytes"
67
"context"
78
"fmt"
89
"net/http"
@@ -29,12 +30,13 @@ import (
2930

3031
func (r *RootCmd) scaletestNotifications() *serpent.Command {
3132
var (
32-
userCount int64
33-
ownerUserPercentage float64
34-
notificationTimeout time.Duration
35-
dialTimeout time.Duration
36-
noCleanup bool
37-
smtpAPIURL string
33+
userCount int64
34+
templateAdminPercentage float64
35+
notificationTimeout time.Duration
36+
smtpRequestTimeout time.Duration
37+
dialTimeout time.Duration
38+
noCleanup bool
39+
smtpAPIURL string
3840

3941
tracingFlags = &scaletestTracingFlags{}
4042

@@ -77,24 +79,24 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
7779
return xerrors.Errorf("--user-count must be greater than 0")
7880
}
7981

80-
if ownerUserPercentage < 0 || ownerUserPercentage > 100 {
81-
return xerrors.Errorf("--owner-user-percentage must be between 0 and 100")
82+
if templateAdminPercentage < 0 || templateAdminPercentage > 100 {
83+
return xerrors.Errorf("--template-admin-percentage must be between 0 and 100")
8284
}
8385

8486
if smtpAPIURL != "" && !strings.HasPrefix(smtpAPIURL, "http://") && !strings.HasPrefix(smtpAPIURL, "https://") {
8587
return xerrors.Errorf("--smtp-api-url must start with http:// or https://")
8688
}
8789

88-
ownerUserCount := int64(float64(userCount) * ownerUserPercentage / 100)
89-
if ownerUserCount == 0 && ownerUserPercentage > 0 {
90-
ownerUserCount = 1
90+
templateAdminCount := int64(float64(userCount) * templateAdminPercentage / 100)
91+
if templateAdminCount == 0 && templateAdminPercentage > 0 {
92+
templateAdminCount = 1
9193
}
92-
regularUserCount := userCount - ownerUserCount
94+
regularUserCount := userCount - templateAdminCount
9395

9496
_, _ = fmt.Fprintf(inv.Stderr, "Distribution plan:\n")
9597
_, _ = fmt.Fprintf(inv.Stderr, " Total users: %d\n", userCount)
96-
_, _ = fmt.Fprintf(inv.Stderr, " Owner users: %d (%.1f%%)\n", ownerUserCount, ownerUserPercentage)
97-
_, _ = fmt.Fprintf(inv.Stderr, " Regular users: %d (%.1f%%)\n", regularUserCount, 100.0-ownerUserPercentage)
98+
_, _ = fmt.Fprintf(inv.Stderr, " Template admins: %d (%.1f%%)\n", templateAdminCount, templateAdminPercentage)
99+
_, _ = fmt.Fprintf(inv.Stderr, " Regular users: %d (%.1f%%)\n", regularUserCount, 100.0-templateAdminPercentage)
98100

99101
outputs, err := output.parse()
100102
if err != nil {
@@ -127,13 +129,12 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
127129
_, _ = fmt.Fprintln(inv.Stderr, "Creating users...")
128130

129131
dialBarrier := &sync.WaitGroup{}
130-
ownerWatchBarrier := &sync.WaitGroup{}
132+
templateAdminWatchBarrier := &sync.WaitGroup{}
131133
dialBarrier.Add(int(userCount))
132-
ownerWatchBarrier.Add(int(ownerUserCount))
134+
templateAdminWatchBarrier.Add(int(templateAdminCount))
133135

134136
expectedNotificationIDs := map[uuid.UUID]struct{}{
135-
notificationsLib.TemplateUserAccountCreated: {},
136-
notificationsLib.TemplateUserAccountDeleted: {},
137+
notificationsLib.TemplateTemplateDeleted: {},
137138
}
138139

139140
triggerTimes := make(map[uuid.UUID]chan time.Time, len(expectedNotificationIDs))
@@ -142,19 +143,20 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
142143
}
143144

144145
configs := make([]notifications.Config, 0, userCount)
145-
for range ownerUserCount {
146+
for range templateAdminCount {
146147
config := notifications.Config{
147148
User: createusers.Config{
148149
OrganizationID: me.OrganizationIDs[0],
149150
},
150-
Roles: []string{codersdk.RoleOwner},
151+
Roles: []string{codersdk.RoleTemplateAdmin},
151152
NotificationTimeout: notificationTimeout,
152153
DialTimeout: dialTimeout,
153154
DialBarrier: dialBarrier,
154-
ReceivingWatchBarrier: ownerWatchBarrier,
155+
ReceivingWatchBarrier: templateAdminWatchBarrier,
155156
ExpectedNotificationsIDs: expectedNotificationIDs,
156157
Metrics: metrics,
157158
SMTPApiURL: smtpAPIURL,
159+
SMTPRequestTimeout: smtpRequestTimeout,
158160
}
159161
if err := config.Validate(); err != nil {
160162
return xerrors.Errorf("validate config: %w", err)
@@ -170,17 +172,16 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
170172
NotificationTimeout: notificationTimeout,
171173
DialTimeout: dialTimeout,
172174
DialBarrier: dialBarrier,
173-
ReceivingWatchBarrier: ownerWatchBarrier,
175+
ReceivingWatchBarrier: templateAdminWatchBarrier,
174176
Metrics: metrics,
175-
SMTPApiURL: smtpAPIURL,
176177
}
177178
if err := config.Validate(); err != nil {
178179
return xerrors.Errorf("validate config: %w", err)
179180
}
180181
configs = append(configs, config)
181182
}
182183

183-
go triggerUserNotifications(
184+
go triggerNotifications(
184185
ctx,
185186
logger,
186187
client,
@@ -261,23 +262,30 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
261262
Required: true,
262263
},
263264
{
264-
Flag: "owner-user-percentage",
265-
Env: "CODER_SCALETEST_NOTIFICATION_OWNER_USER_PERCENTAGE",
265+
Flag: "template-admin-percentage",
266+
Env: "CODER_SCALETEST_NOTIFICATION_TEMPLATE_ADMIN_PERCENTAGE",
266267
Default: "20.0",
267-
Description: "Percentage of users to assign Owner role to (0-100).",
268-
Value: serpent.Float64Of(&ownerUserPercentage),
268+
Description: "Percentage of users to assign Template Admin role to (0-100).",
269+
Value: serpent.Float64Of(&templateAdminPercentage),
269270
},
270271
{
271272
Flag: "notification-timeout",
272273
Env: "CODER_SCALETEST_NOTIFICATION_TIMEOUT",
273-
Default: "5m",
274+
Default: "10m",
274275
Description: "How long to wait for notifications after triggering.",
275276
Value: serpent.DurationOf(&notificationTimeout),
276277
},
278+
{
279+
Flag: "smtp-request-timeout",
280+
Env: "CODER_SCALETEST_SMTP_REQUEST_TIMEOUT",
281+
Default: "5m",
282+
Description: "Timeout for SMTP requests.",
283+
Value: serpent.DurationOf(&smtpRequestTimeout),
284+
},
277285
{
278286
Flag: "dial-timeout",
279287
Env: "CODER_SCALETEST_DIAL_TIMEOUT",
280-
Default: "2m",
288+
Default: "10m",
281289
Description: "Timeout for dialing the notification websocket endpoint.",
282290
Value: serpent.DurationOf(&dialTimeout),
283291
},
@@ -379,9 +387,9 @@ func computeNotificationLatencies(
379387
return nil
380388
}
381389

382-
// triggerUserNotifications waits for all test users to connect,
383-
// then creates and deletes a test user to trigger notification events for testing.
384-
func triggerUserNotifications(
390+
// triggerNotifications waits for all test users to connect,
391+
// then creates and deletes a test template to trigger notification events for testing.
392+
func triggerNotifications(
385393
ctx context.Context,
386394
logger slog.Logger,
387395
client *codersdk.Client,
@@ -414,34 +422,49 @@ func triggerUserNotifications(
414422
return
415423
}
416424

417-
const (
418-
triggerUsername = "scaletest-trigger-user"
419-
triggerEmail = "scaletest-trigger@example.com"
420-
)
425+
logger.Info(ctx, "creating test template to test notifications")
426+
427+
// Upload empty template file.
428+
file, err := client.Upload(ctx, codersdk.ContentTypeTar, bytes.NewReader([]byte{}))
429+
if err != nil {
430+
logger.Error(ctx, "upload test template", slog.Error(err))
431+
return
432+
}
433+
logger.Info(ctx, "test template uploaded", slog.F("file_id", file.ID))
421434

422-
logger.Info(ctx, "creating test user to test notifications",
423-
slog.F("username", triggerUsername),
424-
slog.F("email", triggerEmail),
425-
slog.F("org_id", orgID))
435+
// Create template version.
436+
version, err := client.CreateTemplateVersion(ctx, orgID, codersdk.CreateTemplateVersionRequest{
437+
StorageMethod: codersdk.ProvisionerStorageMethodFile,
438+
FileID: file.ID,
439+
Provisioner: codersdk.ProvisionerTypeEcho,
440+
})
441+
if err != nil {
442+
logger.Error(ctx, "create test template version", slog.Error(err))
443+
return
444+
}
445+
logger.Info(ctx, "test template version created", slog.F("template_version_id", version.ID))
426446

427-
testUser, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{
428-
OrganizationIDs: []uuid.UUID{orgID},
429-
Username: triggerUsername,
430-
Email: triggerEmail,
431-
Password: "test-password-123",
447+
// Create template.
448+
testTemplate, err := client.CreateTemplate(ctx, orgID, codersdk.CreateTemplateRequest{
449+
Name: "scaletest-test-template",
450+
Description: "scaletest-test-template",
451+
VersionID: version.ID,
432452
})
433453
if err != nil {
434-
logger.Error(ctx, "create test user", slog.Error(err))
454+
logger.Error(ctx, "create test template", slog.Error(err))
435455
return
436456
}
437-
expectedNotifications[notificationsLib.TemplateUserAccountCreated] <- time.Now()
457+
logger.Info(ctx, "test template created", slog.F("template_id", testTemplate.ID))
438458

439-
err = client.DeleteUser(ctx, testUser.ID)
459+
// Delete template to trigger notification.
460+
err = client.DeleteTemplate(ctx, testTemplate.ID)
440461
if err != nil {
441-
logger.Error(ctx, "delete test user", slog.Error(err))
462+
logger.Error(ctx, "delete test template", slog.Error(err))
442463
return
443464
}
444-
expectedNotifications[notificationsLib.TemplateUserAccountDeleted] <- time.Now()
445-
close(expectedNotifications[notificationsLib.TemplateUserAccountCreated])
446-
close(expectedNotifications[notificationsLib.TemplateUserAccountDeleted])
465+
logger.Info(ctx, "test template deleted", slog.F("template_id", testTemplate.ID))
466+
467+
// Record expected notification.
468+
expectedNotifications[notificationsLib.TemplateTemplateDeleted] <- time.Now()
469+
close(expectedNotifications[notificationsLib.TemplateTemplateDeleted])
447470
}

scaletest/notifications/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ type Config struct {
3737

3838
// SMTPApiUrl is the URL of the SMTP mock HTTP API
3939
SMTPApiURL string `json:"smtp_api_url"`
40+
41+
// SMTPRequestTimeout is the timeout for SMTP requests.
42+
SMTPRequestTimeout time.Duration `json:"smtp_request_timeout"`
4043
}
4144

4245
func (c Config) Validate() error {
@@ -61,6 +64,10 @@ func (c Config) Validate() error {
6164
return xerrors.New("notification_timeout must be greater than 0")
6265
}
6366

67+
if c.SMTPApiURL != "" && c.SMTPRequestTimeout <= 0 {
68+
return xerrors.New("smtp_request_timeout must be set if smtp_api_url is set")
69+
}
70+
6471
if c.DialTimeout <= 0 {
6572
return xerrors.New("dial_timeout must be greater than 0")
6673
}

scaletest/notifications/metrics.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ func NewMetrics(reg prometheus.Registerer) *Metrics {
2828
Subsystem: "scaletest",
2929
Name: "notification_delivery_latency_seconds",
3030
Help: "Time between notification-creating action and receipt of notification by client",
31+
Buckets: []float64{
32+
1, 5, 10, 30, 60,
33+
120, 180, 240, 300, 360, 420, 480, 540, 600, 660, 720, 780, 840, 900,
34+
1200, 1500, 1800, 2100, 2400, 2700, 3000, 3300, 3600, 3900, 4200, 4500,
35+
5400, 7200,
36+
},
3137
}, []string{"notification_id", "notification_type"})
3238
errors := prometheus.NewCounterVec(prometheus.CounterOpts{
3339
Namespace: "coderd",

scaletest/notifications/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ func (r *Runner) watchNotificationsSMTP(ctx context.Context, user codersdk.User,
299299

300300
apiURL := fmt.Sprintf("%s/messages?email=%s", r.cfg.SMTPApiURL, user.Email)
301301
httpClient := &http.Client{
302-
Timeout: 10 * time.Second,
302+
Timeout: r.cfg.SMTPRequestTimeout,
303303
}
304304

305305
const smtpPollInterval = 2 * time.Second

scaletest/notifications/run_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ func TestRunWithSMTP(t *testing.T) {
228228
ReceivingWatchBarrier: receivingWatchBarrier,
229229
ExpectedNotificationsIDs: expectedNotificationsIDs,
230230
SMTPApiURL: smtpAPIServer.URL,
231+
SMTPRequestTimeout: testutil.WaitLong,
231232
}
232233
err := runnerCfg.Validate()
233234
require.NoError(t, err)

0 commit comments

Comments
 (0)