diff --git a/controllers/execution_verification.go b/controllers/execution_verification.go new file mode 100644 index 0000000..eb488b4 --- /dev/null +++ b/controllers/execution_verification.go @@ -0,0 +1,68 @@ +package controllers + +import ( + "encoding/json" + "oc-scheduler/infrastructure" + + oclib "cloud.o-forge.io/core/oc-lib" + "cloud.o-forge.io/core/oc-lib/models/execution_verification" + "cloud.o-forge.io/core/oc-lib/models/resources/native_tools" + "cloud.o-forge.io/core/oc-lib/tools" + beego "github.com/beego/beego/v2/server/web" +) + +// Operations about workspace +type ExecutionVerificationController struct { + beego.Controller +} + +// @Title GetAll +// @Description find verification by id +// @Param is_draft query string false "draft wished" +// @Success 200 {booking} models.booking +// @router / [get] +func (o *ExecutionVerificationController) GetAll() { + user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request) + isDraft := o.Ctx.Input.Query("is_draft") + o.Data["json"] = oclib.NewRequest(oclib.LibDataEnum(oclib.EXECUTION_VERIFICATION), user, peerID, groups, nil).LoadAll(isDraft == "true") + o.ServeJSON() +} + +// @Title Get +// @Description find verification by id +// @Param id path string true "the id you want to get" +// @Success 200 {booking} models.booking +// @router /:id [get] +func (o *ExecutionVerificationController) Get() { + user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request) + id := o.Ctx.Input.Param(":id") + o.Data["json"] = oclib.NewRequest(oclib.LibDataEnum(oclib.EXECUTION_VERIFICATION), user, peerID, groups, nil).LoadOne(id) + o.ServeJSON() +} + +// @Title Update +// @Description create computes +// @Param id path string true "the compute id you want to get" +// @Param body body models.compute true "The compute content" +// @Success 200 {compute} models.compute +// @router /:id [put] +func (o *ExecutionVerificationController) Put() { + user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request) + // store and return Id or post with UUID + var res map[string]interface{} + id := o.Ctx.Input.Param(":id") + json.Unmarshal(o.Ctx.Input.CopyBody(10000), &res) + data := oclib.NewRequest(oclib.LibDataEnum(oclib.EXECUTION_VERIFICATION), user, peerID, groups, nil).UpdateOne(res, id) + if data.Err == "" && data.Data != nil && data.Data.(*execution_verification.ExecutionVerification).Validate { + data, _ := json.Marshal(&native_tools.WorkflowEventParams{ + WorkflowResourceID: data.Data.(*execution_verification.ExecutionVerification).WorkflowID, + }) + infrastructure.EmitNATS(peerID, tools.PropalgationMessage{ + Action: tools.PubSubAction(tools.WORKFLOW_EVENT), + DataType: tools.EXECUTION_VERIFICATION.EnumIndex(), + Payload: data, + }) + } + o.Data["json"] = data + o.ServeJSON() +} diff --git a/controllers/workflow_sheduler.go b/controllers/workflow_sheduler.go index 1319ba5..dd469b3 100644 --- a/controllers/workflow_sheduler.go +++ b/controllers/workflow_sheduler.go @@ -39,18 +39,8 @@ func (o *WorkflowSchedulerController) Schedule() { var resp *infrastructure.WorkflowSchedule json.Unmarshal(o.Ctx.Input.CopyBody(100000), &resp) - caller := tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{ // paths to call other OC services - tools.PEER: { - tools.POST: "/status/", - }, - tools.BOOKING: { - tools.GET: "/booking/check/:id/:start_date/:end_date", - tools.POST: "/booking/", - }, - }) - logger.Info().Msg("Booking for " + wfId) - req := oclib.NewRequestAdmin(collection, caller) + req := oclib.NewRequestAdmin(collection, nil) // req := oclib.NewRequest(collection, user, peerID, groups, caller) resp.UUID = uuid.New().String() fmt.Println(user, peerID, groups) @@ -58,7 +48,7 @@ func (o *WorkflowSchedulerController) Schedule() { Username: user, PeerID: peerID, Groups: groups, - Caller: caller, + Caller: nil, Admin: true, }) if err != nil { @@ -87,13 +77,24 @@ var wsUpgrader = gorillaws.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } -// CheckStreamHandler is a plain http.HandlerFunc (registered via beego.Handler -// to avoid Beego's WriteHeader interference with the WebSocket upgrade). -// Path: /oc/:id/check → parts = ["", "oc", "", "check"] +// @Title CheckStream +// @Description WebSocket stream for slot availability checking. +// @Param id path string true "workflow id" +// @Param as_possible query bool false "search from now" +// @Param preemption query bool false "force-valid, surface warnings" +// @router /:id/check [get] +func (o *WorkflowSchedulerController) CheckStream() { + CheckStreamHandler(o.Ctx.ResponseWriter, o.Ctx.Request) +} + +// CheckStreamHandler is the WebSocket handler for slot availability checking. +// It is invoked via the CheckStream controller method. // Query params: as_possible=true, preemption=true func CheckStreamHandler(w http.ResponseWriter, r *http.Request) { - parts := strings.Split(strings.TrimSuffix(r.URL.Path, "/"), "/") - wfID := parts[len(parts)-2] // second-to-last segment + wfID := strings.TrimSuffix( + strings.TrimPrefix(r.URL.Path, "/oc/"), + "/check", + ) q := r.URL.Query() asap := q.Get("as_possible") == "true" @@ -136,20 +137,23 @@ func CheckStreamHandler(w http.ResponseWriter, r *http.Request) { plannerCh, plannerUnsub := infrastructure.SubscribePlannerUpdates(watchedPeers) wfCh, wfUnsub := infrastructure.SubscribeWorkflowUpdates(wfID) - // Cleanup on exit: cancel subscriptions, evict planner cache entries, - // signal PB_CLOSE_PLANNER on NATS for each peer that was being watched. + // Unique ID for this check session — used to track refresh ownership. + sessionID := uuid.New().String() + + // Request a fresh planner snapshot for every concerned peer. + // The first session to claim a peer becomes its refresh owner; others skip + // the duplicate PB_PLANNER emission. ownedPeers grows if the workflow's + // peer list changes (wfCh). + ownedPeers := infrastructure.RequestPlannerRefresh(watchedPeers, sessionID) + + // Cleanup on exit (clean or forced): release refresh ownership for the + // peers this session claimed, which resets Refreshing state and emits + // PB_CLOSE_PLANNER so oc-discovery stops the planner stream. defer func() { conn.Close() plannerUnsub() wfUnsub() - for _, peer := range watchedPeers { - if b, err := json.Marshal(map[string]interface{}{"peer_id": peer}); err == nil { - infrastructure.EmitNATS(peer, tools.PropalgationMessage{ - Action: tools.PB_CLOSE_PLANNER, - Payload: b, - }) - } - } + infrastructure.ReleaseRefreshOwnership(ownedPeers, sessionID) }() push := func() error { @@ -166,20 +170,38 @@ func CheckStreamHandler(w http.ResponseWriter, r *http.Request) { return } - // Detect client-side close in a separate goroutine. + // Read loop: detect client-side close and parse schedule parameter + // updates (date changes, booking mode changes, …) sent by the client. + updateCh := make(chan infrastructure.WorkflowSchedule, 1) closeCh := make(chan struct{}) go func() { defer close(closeCh) for { - if _, _, err := conn.ReadMessage(); err != nil { + var updated infrastructure.WorkflowSchedule + if err := conn.ReadJSON(&updated); err != nil { + // Connection closed or unrecoverable read error. return } + // Drop the oldest pending update if the consumer hasn't caught up. + select { + case updateCh <- updated: + default: + <-updateCh + updateCh <- updated + } } }() // Stream loop. for { select { + case updated := <-updateCh: + // The client changed the requested date/params: adopt the new + // schedule and re-run the check immediately. + ws = updated + if err := push(); err != nil { + return + } case <-wfCh: // The workflow was modified: refresh the peer list and re-subscribe // so the stream watches the correct set of planners going forward. @@ -187,11 +209,15 @@ func CheckStreamHandler(w http.ResponseWriter, r *http.Request) { plannerUnsub() watchedPeers = newPeers plannerCh, plannerUnsub = infrastructure.SubscribePlannerUpdates(newPeers) + // Claim refresh ownership for any newly added peers. + newOwned := infrastructure.RequestPlannerRefresh(newPeers, sessionID) + ownedPeers = append(ownedPeers, newOwned...) } if err := push(); err != nil { return } case <-plannerCh: + // A planner snapshot arrived (or was evicted): re-evaluate. if err := push(); err != nil { return } diff --git a/go.mod b/go.mod index 0d26377..ac0c67a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module oc-scheduler go 1.25.0 require ( - cloud.o-forge.io/core/oc-lib v0.0.0-20260304145747-e03a0d3dd0aa + cloud.o-forge.io/core/oc-lib v0.0.0-20260312105633-a30173921f67 github.com/beego/beego/v2 v2.3.8 github.com/google/uuid v1.6.0 github.com/robfig/cron v1.2.0 diff --git a/go.sum b/go.sum index 50aecff..dccec6e 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,18 @@ cloud.o-forge.io/core/oc-lib v0.0.0-20260224093610-a9ebad78f3a8 h1:xoC5PAz1469Qx cloud.o-forge.io/core/oc-lib v0.0.0-20260224093610-a9ebad78f3a8/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA= cloud.o-forge.io/core/oc-lib v0.0.0-20260304145747-e03a0d3dd0aa h1:1wCpI4dwN1pj6MlpJ7/WifhHVHmCE4RU+9klwqgo/bk= cloud.o-forge.io/core/oc-lib v0.0.0-20260304145747-e03a0d3dd0aa/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA= +cloud.o-forge.io/core/oc-lib v0.0.0-20260311072518-933b7147e908 h1:1jz3xI/u2FzCG8phY7ShqADrmCj0mlrdjbdNUosSwgs= +cloud.o-forge.io/core/oc-lib v0.0.0-20260311072518-933b7147e908/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA= +cloud.o-forge.io/core/oc-lib v0.0.0-20260311084029-97bfb0582a99 h1:60BGJeR9uvpDwvNeWqVBnB2JjWLOZv16sUGZjzXSQlg= +cloud.o-forge.io/core/oc-lib v0.0.0-20260311084029-97bfb0582a99/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA= +cloud.o-forge.io/core/oc-lib v0.0.0-20260312073634-2c9c42dd516a h1:oCkb9l/Cvn0x6iicxIydrjfCNU+UHhKuklFgfzDa174= +cloud.o-forge.io/core/oc-lib v0.0.0-20260312073634-2c9c42dd516a/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA= +cloud.o-forge.io/core/oc-lib v0.0.0-20260312083310-f5e199132416 h1:QHR5pzCI/HUawu8pst5Ggio6WPCUUf8XYjNMVk8kSqo= +cloud.o-forge.io/core/oc-lib v0.0.0-20260312083310-f5e199132416/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA= +cloud.o-forge.io/core/oc-lib v0.0.0-20260312104524-e28b79ac0d62 h1:sHzacZxPIKHyjL4EkgG/c7MI8gM1xmLdhaoUx2ZsH+M= +cloud.o-forge.io/core/oc-lib v0.0.0-20260312104524-e28b79ac0d62/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA= +cloud.o-forge.io/core/oc-lib v0.0.0-20260312105633-a30173921f67 h1:x6klvxiRpU1KcvmygIcHGDHFW3CbWC05El6Fryvr3uo= +cloud.o-forge.io/core/oc-lib v0.0.0-20260312105633-a30173921f67/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/beego/beego/v2 v2.3.8 h1:wplhB1pF4TxR+2SS4PUej8eDoH4xGfxuHfS7wAk9VBc= github.com/beego/beego/v2 v2.3.8/go.mod h1:8vl9+RrXqvodrl9C8yivX1e6le6deCK6RWeq8R7gTTg= diff --git a/infrastructure/nats.go b/infrastructure/nats.go index cc3dbd5..1b48d6a 100644 --- a/infrastructure/nats.go +++ b/infrastructure/nats.go @@ -29,8 +29,20 @@ const plannerTTL = 24 * time.Hour // Planner cache — protected by plannerMu // --------------------------------------------------------------------------- +// plannerEntry wraps a planner snapshot with refresh-ownership tracking. +// At most one check session may be the "refresh owner" of a given peer's +// planner at a time: it emits PB_PLANNER to request a fresh snapshot from +// oc-discovery and, on close (clean or forced), emits PB_CLOSE_PLANNER to +// release the stream. Any subsequent session that needs the same peer's +// planner will see Refreshing=true and skip the duplicate request. +type plannerEntry struct { + Planner *planner.Planner + Refreshing bool // true while a PB_PLANNER request is in flight + RefreshOwner string // session UUID that initiated the current refresh +} + var plannerMu sync.RWMutex -var PlannerCache = map[string]*planner.Planner{} +var PlannerCache = map[string]*plannerEntry{} var plannerAddedAt = map[string]time.Time{} // peerID → first-seen timestamp // --------------------------------------------------------------------------- @@ -104,29 +116,39 @@ func notify(mu *sync.RWMutex, registry map[string][]chan struct{}, key string) { // Cache helpers // --------------------------------------------------------------------------- -// storePlanner inserts or updates a planner for peerID. +// storePlanner inserts or updates the planner snapshot for peerID. // On first insertion it schedules an automatic eviction after plannerTTL. +// Existing refresh-ownership state (Refreshing / RefreshOwner) is preserved +// so that an in-flight request is not inadvertently reset. // All subscribers interested in this peer are notified. func storePlanner(peerID string, p *planner.Planner) { plannerMu.Lock() - isNew := PlannerCache[peerID] == nil - PlannerCache[peerID] = p + entry := PlannerCache[peerID] + isNew := entry == nil if isNew { + entry = &plannerEntry{} + PlannerCache[peerID] = entry plannerAddedAt[peerID] = time.Now() go evictAfter(peerID, plannerTTL) } + entry.Planner = p plannerMu.Unlock() notifyPlannerWatchers(peerID) } -// evictAfter waits ttl from the first-seen time for peerID then emits a -// PB_CLOSE_PLANNER event, which removes the entry from the cache and notifies -// NATS. +// evictAfter waits ttl from first insertion then deletes the cache entry and +// emits PB_CLOSE_PLANNER so oc-discovery stops streaming for this peer. +// This is the only path that actually removes an entry from PlannerCache; +// session close (ReleaseRefreshOwnership) only resets ownership state. func evictAfter(peerID string, ttl time.Duration) { time.Sleep(ttl) - plannerMu.RLock() + plannerMu.Lock() _, exists := PlannerCache[peerID] - plannerMu.RUnlock() + if exists { + delete(PlannerCache, peerID) + delete(plannerAddedAt, peerID) + } + plannerMu.Unlock() if exists { EmitNATS(peerID, tools.PropalgationMessage{Action: tools.PB_CLOSE_PLANNER}) } @@ -137,12 +159,11 @@ func evictAfter(peerID string, ttl time.Duration) { // --------------------------------------------------------------------------- func EmitNATS(peerID string, message tools.PropalgationMessage) { + // PB_CLOSE_PLANNER: notify local watchers so streams re-evaluate. + // Cache mutations (eviction or ownership reset) are the caller's + // responsibility — see evictAfter and ReleaseRefreshOwnership. if message.Action == tools.PB_CLOSE_PLANNER { - plannerMu.Lock() - delete(PlannerCache, peerID) - delete(plannerAddedAt, peerID) - plannerMu.Unlock() - notifyPlannerWatchers(peerID) // let streams re-evaluate (will warn "no planner") + notifyPlannerWatchers(peerID) } b, _ := json.Marshal(message) tools.NewNATSCaller().SetNATSPub(tools.PROPALGATION_EVENT, tools.NATSResponse{ @@ -309,6 +330,17 @@ func ListenNATS() { // considered in the target WorkflowExecution (BookingsState / PurchasesState). // WORKFLOW_EXECUTION → the execution reached SCHEDULED; confirm all // local draft bookings and purchases listed in its states. + tools.PLANNER_EXECUTION: func(resp tools.NATSResponse) { + m := map[string]interface{}{} + p := planner.Planner{} + if err := json.Unmarshal(resp.Payload, &m); err != nil { + return + } + if err := json.Unmarshal(resp.Payload, &p); err != nil { + return + } + storePlanner(fmt.Sprintf("%v", m["peer_id"]), &p) + }, tools.PROPALGATION_EVENT: func(resp tools.NATSResponse) { if resp.FromApp != "oc-discovery" { return @@ -318,16 +350,6 @@ func ListenNATS() { return } switch prop.Action { - case tools.PB_PLANNER: - m := map[string]interface{}{} - p := planner.Planner{} - if err := json.Unmarshal(prop.Payload, &m); err != nil { - return - } - if err := json.Unmarshal(prop.Payload, &p); err != nil { - return - } - storePlanner(fmt.Sprintf("%v", m["peer_id"]), &p) case tools.PB_CONSIDERS: switch tools.DataType(prop.DataType) { case tools.BOOKING, tools.PURCHASE_RESOURCE: @@ -379,9 +401,9 @@ func ListenNATS() { } // Verify the slot is free in our planner (if we have one). plannerMu.RLock() - p := PlannerCache[self.PeerID] + selfEntry := PlannerCache[self.PeerID] plannerMu.RUnlock() - if p != nil && !checkInstance(p, bk.ResourceID, bk.InstanceID, bk.ExpectedStartDate, bk.ExpectedEndDate) { + if selfEntry != nil && selfEntry.Planner != nil && !checkInstance(selfEntry.Planner, bk.ResourceID, bk.InstanceID, bk.ExpectedStartDate, bk.ExpectedEndDate) { fmt.Println("ListenNATS: booking conflicts with local planner, discarding") return } @@ -598,6 +620,60 @@ func refreshSelfPlanner(peerID string, request *tools.APIRequest) { // Planner broadcast // --------------------------------------------------------------------------- +// RequestPlannerRefresh asks oc-discovery for a fresh planner snapshot for +// each peer in peerIDs. Only the first session to request a given peer becomes +// its "refresh owner": subsequent sessions see Refreshing=true and skip the +// duplicate PB_PLANNER emission. Returns the subset of peerIDs for which this +// session claimed ownership (needed to release on close). +func RequestPlannerRefresh(peerIDs []string, sessionID string) []string { + var owned []string + for _, peerID := range peerIDs { + plannerMu.Lock() + entry := PlannerCache[peerID] + if entry == nil { + entry = &plannerEntry{} + PlannerCache[peerID] = entry + plannerAddedAt[peerID] = time.Now() + go evictAfter(peerID, plannerTTL) + } + shouldRequest := !entry.Refreshing + if shouldRequest { + entry.Refreshing = true + entry.RefreshOwner = sessionID + } + plannerMu.Unlock() + if shouldRequest { + owned = append(owned, peerID) + payload, _ := json.Marshal(map[string]any{"peer_id": peerID}) + EmitNATS(peerID, tools.PropalgationMessage{ + Action: tools.PB_PLANNER, + Payload: payload, + }) + } + } + return owned +} + +// ReleaseRefreshOwnership is called when a check session closes (clean or +// forced). For each peer this session owns, it resets the refresh state and +// emits PB_CLOSE_PLANNER so oc-discovery stops the planner stream. +// The planner data itself stays in the cache until TTL eviction. +func ReleaseRefreshOwnership(peerIDs []string, sessionID string) { + for _, peerID := range peerIDs { + plannerMu.Lock() + if entry := PlannerCache[peerID]; entry != nil && entry.RefreshOwner == sessionID { + entry.Refreshing = false + entry.RefreshOwner = "" + } + plannerMu.Unlock() + payload, _ := json.Marshal(map[string]any{"peer_id": peerID}) + EmitNATS(peerID, tools.PropalgationMessage{ + Action: tools.PB_CLOSE_PLANNER, + Payload: payload, + }) + } +} + // broadcastPlanner iterates the storage and compute peers of the given workflow // and, for each peer not yet in the cache, emits a PB_PLANNER propagation so // downstream consumers (oc-discovery, other schedulers) refresh their state. @@ -631,7 +707,8 @@ func broadcastPlanner(wf *workflow.Workflow) { cached := PlannerCache[p.PeerID] plannerMu.RUnlock() - if cached == nil { + // Only request if no snapshot and no refresh already in flight. + if cached == nil || (cached.Planner == nil && !cached.Refreshing) { payload, err := json.Marshal(map[string]interface{}{"peer_id": p.PeerID}) if err != nil { continue diff --git a/infrastructure/scheduler.go b/infrastructure/scheduler.go index c1f9ff6..4f9485d 100644 --- a/infrastructure/scheduler.go +++ b/infrastructure/scheduler.go @@ -520,14 +520,14 @@ func collectBookingResources(wf *workflow.Workflow, selectedInstances workflow.C func checkResourceAvailability(res []bookingResource, start time.Time, end *time.Time) (unavailable []string, warnings []string) { for _, r := range res { plannerMu.RLock() - p := PlannerCache[r.peerID] + entry := PlannerCache[r.peerID] plannerMu.RUnlock() - if p == nil { + if entry == nil || entry.Planner == nil { warnings = append(warnings, fmt.Sprintf( "peer %s planner not in cache for resource %s – assuming available", r.peerID, r.id)) continue } - if !checkInstance(p, r.id, r.instanceID, start, end) { + if !checkInstance(entry.Planner, r.id, r.instanceID, start, end) { unavailable = append(unavailable, r.id) warnings = append(warnings, fmt.Sprintf( "resource %s is not available in [%s – %s]", diff --git a/routers/commentsRouter.go b/routers/commentsRouter.go index 2484017..df46195 100644 --- a/routers/commentsRouter.go +++ b/routers/commentsRouter.go @@ -43,6 +43,33 @@ func init() { Filters: nil, Params: nil}) + beego.GlobalControllerRouter["oc-scheduler/controllers:ExecutionVerificationController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:ExecutionVerificationController"], + beego.ControllerComments{ + Method: "GetAll", + Router: `/`, + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Filters: nil, + Params: nil}) + + beego.GlobalControllerRouter["oc-scheduler/controllers:ExecutionVerificationController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:ExecutionVerificationController"], + beego.ControllerComments{ + Method: "Get", + Router: `/:id`, + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Filters: nil, + Params: nil}) + + beego.GlobalControllerRouter["oc-scheduler/controllers:ExecutionVerificationController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:ExecutionVerificationController"], + beego.ControllerComments{ + Method: "Put", + Router: `/:id`, + AllowHTTPMethods: []string{"put"}, + MethodParams: param.Make(), + Filters: nil, + Params: nil}) + beego.GlobalControllerRouter["oc-scheduler/controllers:LokiController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:LokiController"], beego.ControllerComments{ Method: "GetLogs", diff --git a/routers/router.go b/routers/router.go index 914dd00..c87dfb8 100644 --- a/routers/router.go +++ b/routers/router.go @@ -8,7 +8,6 @@ package routers import ( - "net/http" "oc-scheduler/controllers" beego "github.com/beego/beego/v2/server/web" @@ -29,6 +28,11 @@ func init() { &controllers.BookingController{}, ), ), + beego.NSNamespace("/verification", + beego.NSInclude( + &controllers.ExecutionVerificationController{}, + ), + ), beego.NSNamespace("/execution", beego.NSInclude( &controllers.WorkflowExecutionController{}, @@ -42,7 +46,4 @@ func init() { ) beego.AddNamespace(ns) - - // Route WebSocket hors du pipeline Beego pour éviter le WriteHeader parasite - beego.Handler("/oc/:id/check", http.HandlerFunc(controllers.CheckStreamHandler)) }