@@ -257,28 +257,7 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error {
257
257
return xerrors .Errorf ("determine current snapshot: %w" , err )
258
258
}
259
259
260
- presetsMap := make (map [hardLimitedPresetKey ][]database.GetTemplatePresetsWithPrebuildsRow )
261
- for _ , preset := range snapshot .Presets {
262
- key := hardLimitedPresetKey {
263
- orgName : preset .OrganizationName ,
264
- templateName : preset .TemplateName ,
265
- presetName : preset .Name ,
266
- }
267
-
268
- presetsMap [key ] = append (presetsMap [key ], preset )
269
- }
270
-
271
- isPresetHardLimited := make (map [hardLimitedPresetKey ]bool )
272
- for key , presets := range presetsMap {
273
- for _ , preset := range presets {
274
- if preset .UsingActiveVersion && ! preset .Deleted && snapshot .IsHardLimited (preset .ID ) {
275
- isPresetHardLimited [key ] = true
276
- break
277
- }
278
- }
279
- }
280
-
281
- c .metrics .trackHardLimitedStatus (isPresetHardLimited )
260
+ c .reportHardLimitedPresets (snapshot )
282
261
283
262
if len (snapshot .Presets ) == 0 {
284
263
logger .Debug (ctx , "no templates found with prebuilds configured" )
@@ -320,6 +299,56 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error {
320
299
return err
321
300
}
322
301
302
+ // Report a metric only if the preset uses the latest version of the template and the template is not deleted.
303
+ // This avoids conflicts between metrics from old and new template versions.
304
+ //
305
+ // NOTE: Multiple versions of a preset can exist with the same orgName, templateName, and presetName,
306
+ // because templates can have multiple versions — or deleted templates can share the same name.
307
+ //
308
+ // The safest approach is to report the metric only for the latest version of the preset.
309
+ // When a new template version is released, the metric for the new preset should overwrite
310
+ // the old value in Prometheus.
311
+ //
312
+ // However, there’s one edge case: if an admin creates a template, it becomes hard-limited,
313
+ // then deletes the template and never creates another with the same name,
314
+ // the old preset will continue to be reported as hard-limited —
315
+ // even though it’s deleted. This will persist until `coderd` is restarted.
316
+
317
+ func (c * StoreReconciler ) reportHardLimitedPresets (snapshot * prebuilds.GlobalSnapshot ) {
318
+ // presetsMap is a map from key (orgName:templateName:presetName) to list of corresponding presets.
319
+ // Multiple versions of a preset can exist with the same orgName, templateName, and presetName,
320
+ // because templates can have multiple versions — or deleted templates can share the same name.
321
+ presetsMap := make (map [hardLimitedPresetKey ][]database.GetTemplatePresetsWithPrebuildsRow )
322
+ for _ , preset := range snapshot .Presets {
323
+ key := hardLimitedPresetKey {
324
+ orgName : preset .OrganizationName ,
325
+ templateName : preset .TemplateName ,
326
+ presetName : preset .Name ,
327
+ }
328
+
329
+ presetsMap [key ] = append (presetsMap [key ], preset )
330
+ }
331
+
332
+ // Report a preset as hard-limited only if all the following conditions are met:
333
+ // - The preset is marked as hard-limited
334
+ // - The preset is using the active version of its template, and the template has not been deleted
335
+ //
336
+ // The second condition is important because a hard-limited preset that has become outdated is no longer relevant.
337
+ // Its associated prebuilt workspaces were likely deleted, and it's not meaningful to continue reporting it
338
+ // as hard-limited to the admin.
339
+ isPresetHardLimited := make (map [hardLimitedPresetKey ]bool )
340
+ for key , presets := range presetsMap {
341
+ for _ , preset := range presets {
342
+ if preset .UsingActiveVersion && ! preset .Deleted && snapshot .IsHardLimited (preset .ID ) {
343
+ isPresetHardLimited [key ] = true
344
+ break
345
+ }
346
+ }
347
+ }
348
+
349
+ c .metrics .registerHardLimitedPresets (isPresetHardLimited )
350
+ }
351
+
323
352
// SnapshotState captures the current state of all prebuilds across templates.
324
353
func (c * StoreReconciler ) SnapshotState (ctx context.Context , store database.Store ) (* prebuilds.GlobalSnapshot , error ) {
325
354
if err := ctx .Err (); err != nil {
0 commit comments