@@ -697,7 +697,8 @@ func (c *StoreReconciler) createPrebuiltWorkspace(ctx context.Context, prebuiltW
697697return xerrors .Errorf ("failed to generate unique prebuild ID: %w" ,err )
698698}
699699
700- return c .store .InTx (func (db database.Store )error {
700+ var provisionerJob * database.ProvisionerJob
701+ err = c .store .InTx (func (db database.Store )error {
701702template ,err := db .GetTemplateByID (ctx ,templateID )
702703if err != nil {
703704return xerrors .Errorf ("failed to get template: %w" ,err )
@@ -732,11 +733,20 @@ func (c *StoreReconciler) createPrebuiltWorkspace(ctx context.Context, prebuiltW
732733c .logger .Info (ctx ,"attempting to create prebuild" ,slog .F ("name" ,name ),
733734slog .F ("workspace_id" ,prebuiltWorkspaceID .String ()),slog .F ("preset_id" ,presetID .String ()))
734735
735- return c .provision (ctx ,db ,prebuiltWorkspaceID ,template ,presetID ,database .WorkspaceTransitionStart ,workspace ,DeprovisionModeNormal )
736+ provisionerJob ,err = c .provision (ctx ,db ,prebuiltWorkspaceID ,template ,presetID ,database .WorkspaceTransitionStart ,workspace ,DeprovisionModeNormal )
737+ return err
736738},& database.TxOptions {
737739Isolation :sql .LevelRepeatableRead ,
738740ReadOnly :false ,
739741})
742+ if err != nil {
743+ return err
744+ }
745+
746+ // Publish provisioner job event to notify the acquirer that a new job was posted
747+ c .publishProvisionerJob (ctx ,provisionerJob ,prebuiltWorkspaceID )
748+
749+ return nil
740750}
741751
742752// provisionDelete provisions a delete transition for a prebuilt workspace.
@@ -748,26 +758,26 @@ func (c *StoreReconciler) createPrebuiltWorkspace(ctx context.Context, prebuiltW
748758//
749759// IMPORTANT: This function must be called within a database transaction. It does not create its own transaction.
750760// The caller is responsible for managing the transaction boundary via db.InTx().
751- func (c * StoreReconciler )provisionDelete (ctx context.Context ,db database.Store ,workspaceID uuid.UUID ,templateID uuid.UUID ,presetID uuid.UUID ,mode DeprovisionMode )error {
761+ func (c * StoreReconciler )provisionDelete (ctx context.Context ,db database.Store ,workspaceID uuid.UUID ,templateID uuid.UUID ,presetID uuid.UUID ,mode DeprovisionMode )( * database. ProvisionerJob , error ) {
752762workspace ,err := db .GetWorkspaceByID (ctx ,workspaceID )
753763if err != nil {
754- return xerrors .Errorf ("get workspace by ID: %w" ,err )
764+ return nil , xerrors .Errorf ("get workspace by ID: %w" ,err )
755765}
756766
757767template ,err := db .GetTemplateByID (ctx ,templateID )
758768if err != nil {
759- return xerrors .Errorf ("failed to get template: %w" ,err )
769+ return nil , xerrors .Errorf ("failed to get template: %w" ,err )
760770}
761771
762772if workspace .OwnerID != database .PrebuildsSystemUserID {
763- return xerrors .Errorf ("prebuilt workspace is not owned by prebuild user anymore, probably it was claimed" )
773+ return nil , xerrors .Errorf ("prebuilt workspace is not owned by prebuild user anymore, probably it was claimed" )
764774}
765775
766776c .logger .Info (ctx ,"attempting to delete prebuild" ,slog .F ("orphan" ,mode .String ()),
767777slog .F ("name" ,workspace .Name ),slog .F ("workspace_id" ,workspaceID .String ()),slog .F ("preset_id" ,presetID .String ()))
768778
769- return c .provision (ctx ,db ,workspaceID ,template ,presetID ,
770- database . WorkspaceTransitionDelete , workspace , mode )
779+ provisionerJob , err := c .provision (ctx ,db ,workspaceID ,template ,presetID ,database . WorkspaceTransitionDelete , workspace , mode )
780+ return provisionerJob , err
771781}
772782
773783// cancelAndOrphanDeletePendingPrebuilds cancels pending prebuild jobs from inactive template versions
@@ -779,7 +789,8 @@ func (c *StoreReconciler) provisionDelete(ctx context.Context, db database.Store
779789// Since these jobs were never processed by a provisioner, no Terraform resources were created,
780790// making it safe to orphan-delete the workspaces (skipping Terraform destroy).
781791func (c * StoreReconciler )cancelAndOrphanDeletePendingPrebuilds (ctx context.Context ,templateID uuid.UUID ,templateVersionID uuid.UUID ,presetID uuid.UUID )error {
782- return c .store .InTx (func (db database.Store )error {
792+ provisionerJobs := make (map [uuid.UUID ]* database.ProvisionerJob )
793+ err := c .store .InTx (func (db database.Store )error {
783794canceledJobs ,err := db .UpdatePrebuildProvisionerJobWithCancel (
784795ctx ,
785796database.UpdatePrebuildProvisionerJobWithCancelParams {
@@ -808,11 +819,13 @@ func (c *StoreReconciler) cancelAndOrphanDeletePendingPrebuilds(ctx context.Cont
808819
809820var multiErr multierror.Error
810821for _ ,job := range canceledJobs {
811- err = c .provisionDelete (ctx ,db ,job .WorkspaceID ,job .TemplateID ,presetID ,DeprovisionModeOrphan )
822+ provisionerJob , err : =c .provisionDelete (ctx ,db ,job .WorkspaceID ,job .TemplateID ,presetID ,DeprovisionModeOrphan )
812823if err != nil {
813824c .logger .Error (ctx ,"failed to orphan delete canceled prebuild" ,
814825slog .F ("workspace_id" ,job .WorkspaceID .String ()),slog .Error (err ))
815826multiErr .Errors = append (multiErr .Errors ,err )
827+ }else {
828+ provisionerJobs [job .WorkspaceID ]= provisionerJob
816829}
817830}
818831
@@ -821,15 +834,35 @@ func (c *StoreReconciler) cancelAndOrphanDeletePendingPrebuilds(ctx context.Cont
821834Isolation :sql .LevelRepeatableRead ,
822835ReadOnly :false ,
823836})
837+ if err != nil {
838+ return err
839+ }
840+
841+ // Publish provisioner job events to notify the acquirer that new jobs were posted
842+ for workspaceID ,job := range provisionerJobs {
843+ c .publishProvisionerJob (ctx ,job ,workspaceID )
844+ }
845+
846+ return nil
824847}
825848
826849func (c * StoreReconciler )deletePrebuiltWorkspace (ctx context.Context ,prebuiltWorkspaceID uuid.UUID ,templateID uuid.UUID ,presetID uuid.UUID )error {
827- return c .store .InTx (func (db database.Store )error {
828- return c .provisionDelete (ctx ,db ,prebuiltWorkspaceID ,templateID ,presetID ,DeprovisionModeNormal )
850+ var provisionerJob * database.ProvisionerJob
851+ err := c .store .InTx (func (db database.Store ) (err error ) {
852+ provisionerJob ,err = c .provisionDelete (ctx ,db ,prebuiltWorkspaceID ,templateID ,presetID ,DeprovisionModeNormal )
853+ return err
829854},& database.TxOptions {
830855Isolation :sql .LevelRepeatableRead ,
831856ReadOnly :false ,
832857})
858+ if err != nil {
859+ return err
860+ }
861+
862+ // Publish provisioner job event to notify the acquirer that a new job was posted
863+ c .publishProvisionerJob (ctx ,provisionerJob ,prebuiltWorkspaceID )
864+
865+ return nil
833866}
834867
835868func (c * StoreReconciler )provision (
@@ -841,10 +874,10 @@ func (c *StoreReconciler) provision(
841874transition database.WorkspaceTransition ,
842875workspace database.Workspace ,
843876mode DeprovisionMode ,
844- )error {
877+ )( * database. ProvisionerJob , error ) {
845878tvp ,err := db .GetPresetParametersByTemplateVersionID (ctx ,template .ActiveVersionID )
846879if err != nil {
847- return xerrors .Errorf ("fetch preset details: %w" ,err )
880+ return nil , xerrors .Errorf ("fetch preset details: %w" ,err )
848881}
849882
850883var params []codersdk.WorkspaceBuildParameter
@@ -893,26 +926,34 @@ func (c *StoreReconciler) provision(
893926audit.WorkspaceBuildBaggage {},
894927)
895928if err != nil {
896- return xerrors .Errorf ("provision workspace: %w" ,err )
929+ return nil , xerrors .Errorf ("provision workspace: %w" ,err )
897930}
898-
899931if provisionerJob == nil {
900- return nil
901- }
902-
903- // Publish provisioner job event outside of transaction.
904- select {
905- case c .provisionNotifyCh <- * provisionerJob :
906- default :// channel full, drop the message; provisioner will pick this job up later with its periodic check, though.
907- c .logger .Warn (ctx ,"provisioner job notification queue full, dropping" ,
908- slog .F ("job_id" ,provisionerJob .ID ),slog .F ("prebuild_id" ,prebuildID .String ()))
932+ // This should not happen, builder.Build() should either return a job or an error.
933+ // Returning an error to fail fast if we hit this unexpected case.
934+ return nil ,xerrors .Errorf ("provision succeeded but returned no job" )
909935}
910936
911937c .logger .Info (ctx ,"prebuild job scheduled" ,slog .F ("transition" ,transition ),
912938slog .F ("prebuild_id" ,prebuildID .String ()),slog .F ("preset_id" ,presetID .String ()),
913939slog .F ("job_id" ,provisionerJob .ID ))
914940
915- return nil
941+ return provisionerJob ,nil
942+ }
943+
944+ // publishProvisionerJob publishes a provisioner job event to notify the acquirer that a new job has been created.
945+ // This must be called after the database transaction that creates the job has committed to ensure
946+ // the job is visible to provisioners when they query the database.
947+ func (c * StoreReconciler )publishProvisionerJob (ctx context.Context ,provisionerJob * database.ProvisionerJob ,workspaceID uuid.UUID ) {
948+ if provisionerJob == nil {
949+ return
950+ }
951+ select {
952+ case c .provisionNotifyCh <- * provisionerJob :
953+ default :// channel full, drop the message; provisioner will pick this job up later with its periodic check
954+ c .logger .Warn (ctx ,"provisioner job notification queue full, dropping" ,
955+ slog .F ("job_id" ,provisionerJob .ID ),slog .F ("prebuild_id" ,workspaceID .String ()))
956+ }
916957}
917958
918959// ForceMetricsUpdate forces the metrics collector, if defined, to update its state (we cache the metrics state to