This commit is contained in:
mr
2026-03-06 10:13:47 +01:00
parent 29623244c4
commit 98fe2600b3
10 changed files with 445 additions and 29 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"oc-scheduler/infrastructure" "oc-scheduler/infrastructure"
"strings"
oclib "cloud.o-forge.io/core/oc-lib" oclib "cloud.o-forge.io/core/oc-lib"
"cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/dbs"
@@ -86,29 +87,19 @@ var wsUpgrader = gorillaws.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true }, CheckOrigin: func(r *http.Request) bool { return true },
} }
// @Title CheckStream // CheckStreamHandler is a plain http.HandlerFunc (registered via beego.Handler
// @Description WebSocket stream of slot availability for a workflow. // to avoid Beego's WriteHeader interference with the WebSocket upgrade).
// After the handshake the client sends one JSON frame containing the // Path: /oc/:id/check → parts = ["", "oc", "<id>", "check"]
// WorkflowSchedule parameters (start, end, booking_mode, duration_s, …). // Query params: as_possible=true, preemption=true
// The server responds with a CheckResult frame immediately and again each time func CheckStreamHandler(w http.ResponseWriter, r *http.Request) {
// a planner for one of the workflow's storage/compute peers is updated. parts := strings.Split(strings.TrimSuffix(r.URL.Path, "/"), "/")
// When the stream is interrupted the cache entries for those peers are evicted wfID := parts[len(parts)-2] // second-to-last segment
// and a PB_CLOSE_PLANNER event is emitted on NATS.
// Query params:
// - as_possible=true ignore start date, search from now
// - preemption=true validate anyway, raise warnings
//
// @Param id path string true "workflow id"
// @Param as_possible query bool false "find nearest free slot from now"
// @Param preemption query bool false "validate anyway, raise warnings"
// @Success 101
// @router /:id/check [get]
func (o *WorkflowSchedulerController) CheckStream() {
wfID := o.Ctx.Input.Param(":id")
asap, _ := o.GetBool("as_possible", false)
preemption, _ := o.GetBool("preemption", false)
user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request) q := r.URL.Query()
asap := q.Get("as_possible") == "true"
preemption := q.Get("preemption") == "true"
user, peerID, groups := oclib.ExtractTokenInfo(*r)
req := &tools.APIRequest{ req := &tools.APIRequest{
Username: user, Username: user,
PeerID: peerID, PeerID: peerID,
@@ -120,15 +111,16 @@ func (o *WorkflowSchedulerController) CheckStream() {
// Resolve the peer IDs concerned by this workflow before upgrading so we // Resolve the peer IDs concerned by this workflow before upgrading so we
// can abort cleanly with a plain HTTP error if the workflow is not found. // can abort cleanly with a plain HTTP error if the workflow is not found.
watchedPeers, err := infrastructure.GetWorkflowPeerIDs(wfID, req) watchedPeers, err := infrastructure.GetWorkflowPeerIDs(wfID, req)
fmt.Println("Here my watched peers involved in workflow", watchedPeers)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{"code": 404, "error": err.Error()} http.Error(w, `{"code":404,"error":"`+err.Error()+`"}`, http.StatusNotFound)
o.ServeJSON()
return return
} }
// Upgrade to WebSocket. // Upgrade to WebSocket.
conn, err := wsUpgrader.Upgrade(o.Ctx.ResponseWriter, o.Ctx.Request, nil) conn, err := wsUpgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
// gorilla already wrote the error response
return return
} }
@@ -162,6 +154,7 @@ func (o *WorkflowSchedulerController) CheckStream() {
push := func() error { push := func() error {
result, checkErr := ws.Check(wfID, asap, preemption, req) result, checkErr := ws.Check(wfID, asap, preemption, req)
fmt.Println(result, checkErr)
if checkErr != nil { if checkErr != nil {
return checkErr return checkErr
} }

2
go.mod
View File

@@ -3,7 +3,7 @@ module oc-scheduler
go 1.25.0 go 1.25.0
require ( require (
cloud.o-forge.io/core/oc-lib v0.0.0-20260224093610-a9ebad78f3a8 cloud.o-forge.io/core/oc-lib v0.0.0-20260304145747-e03a0d3dd0aa
github.com/beego/beego/v2 v2.3.8 github.com/beego/beego/v2 v2.3.8
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/robfig/cron v1.2.0 github.com/robfig/cron v1.2.0

2
go.sum
View File

@@ -14,6 +14,8 @@ cloud.o-forge.io/core/oc-lib v0.0.0-20260224092928-54aef164ba10 h1:9i8fDtGjg3JDn
cloud.o-forge.io/core/oc-lib v0.0.0-20260224092928-54aef164ba10/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA= cloud.o-forge.io/core/oc-lib v0.0.0-20260224092928-54aef164ba10/go.mod h1:+ENuvBfZdESSvecoqGY/wSvRlT3vinEolxKgwbOhUpA=
cloud.o-forge.io/core/oc-lib v0.0.0-20260224093610-a9ebad78f3a8 h1:xoC5PAz1469QxrNm8rrsq5+BtwshEt+L2Nhf90MrqrM= cloud.o-forge.io/core/oc-lib v0.0.0-20260224093610-a9ebad78f3a8 h1:xoC5PAz1469QxrNm8rrsq5+BtwshEt+L2Nhf90MrqrM=
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-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=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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 h1:wplhB1pF4TxR+2SS4PUej8eDoH4xGfxuHfS7wAk9VBc=
github.com/beego/beego/v2 v2.3.8/go.mod h1:8vl9+RrXqvodrl9C8yivX1e6le6deCK6RWeq8R7gTTg= github.com/beego/beego/v2 v2.3.8/go.mod h1:8vl9+RrXqvodrl9C8yivX1e6le6deCK6RWeq8R7gTTg=

View File

@@ -199,7 +199,7 @@ func (ws *WorkflowSchedule) Schedules(wfID string, request *tools.APIRequest) (*
} }
fmt.Println("Schedules") fmt.Println("Schedules")
wf.GetAccessor(&tools.APIRequest{Admin: true}).UpdateOne(wf, wf.GetID()) wf.GetAccessor(&tools.APIRequest{Admin: true}).UpdateOne(wf.Serialize(wf), wf.GetID())
return ws, wf, executions, nil return ws, wf, executions, nil
} }
@@ -414,10 +414,10 @@ func (ws *WorkflowSchedule) Check(wfID string, asap bool, preemption bool, reque
// 4. Extract booking-relevant (storage + compute) resources from the graph, // 4. Extract booking-relevant (storage + compute) resources from the graph,
// resolving the selected instance for each resource. // resolving the selected instance for each resource.
checkables := collectBookingResources(wf, ws.SelectedInstances) checkables := collectBookingResources(wf, ws.SelectedInstances)
fmt.Println(checkables)
// 5. Check every resource against its peer's planner // 5. Check every resource against its peer's planner
unavailable, warnings := checkResourceAvailability(checkables, start, end) unavailable, warnings := checkResourceAvailability(checkables, start, end)
fmt.Println(unavailable, warnings)
result := &CheckResult{ result := &CheckResult{
Start: start, Start: start,
End: end, End: end,

Binary file not shown.

View File

@@ -7,6 +7,42 @@ import (
func init() { func init() {
beego.GlobalControllerRouter["oc-scheduler/controllers:BookingController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:BookingController"],
beego.ControllerComments{
Method: "GetAll",
Router: `/`,
AllowHTTPMethods: []string{"get"},
MethodParams: param.Make(),
Filters: nil,
Params: nil})
beego.GlobalControllerRouter["oc-scheduler/controllers:BookingController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:BookingController"],
beego.ControllerComments{
Method: "Get",
Router: `/:id`,
AllowHTTPMethods: []string{"get"},
MethodParams: param.Make(),
Filters: nil,
Params: nil})
beego.GlobalControllerRouter["oc-scheduler/controllers:BookingController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:BookingController"],
beego.ControllerComments{
Method: "Search",
Router: `/search/:start_date/:end_date`,
AllowHTTPMethods: []string{"get"},
MethodParams: param.Make(),
Filters: nil,
Params: nil})
beego.GlobalControllerRouter["oc-scheduler/controllers:BookingController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:BookingController"],
beego.ControllerComments{
Method: "ExecutionSearch",
Router: `/search/execution/:id`,
AllowHTTPMethods: []string{"get"},
MethodParams: param.Make(),
Filters: nil,
Params: nil})
beego.GlobalControllerRouter["oc-scheduler/controllers:LokiController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:LokiController"], beego.GlobalControllerRouter["oc-scheduler/controllers:LokiController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:LokiController"],
beego.ControllerComments{ beego.ControllerComments{
Method: "GetLogs", Method: "GetLogs",
@@ -88,6 +124,15 @@ func init() {
Filters: nil, Filters: nil,
Params: nil}) Params: nil})
beego.GlobalControllerRouter["oc-scheduler/controllers:WorkflowSchedulerController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:WorkflowSchedulerController"],
beego.ControllerComments{
Method: "CheckStream",
Router: `/:id/check`,
AllowHTTPMethods: []string{"get"},
MethodParams: param.Make(),
Filters: nil,
Params: nil})
beego.GlobalControllerRouter["oc-scheduler/controllers:WorkflowSchedulerController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:WorkflowSchedulerController"], beego.GlobalControllerRouter["oc-scheduler/controllers:WorkflowSchedulerController"] = append(beego.GlobalControllerRouter["oc-scheduler/controllers:WorkflowSchedulerController"],
beego.ControllerComments{ beego.ControllerComments{
Method: "SearchScheduledDraftOrder", Method: "SearchScheduledDraftOrder",

View File

@@ -8,6 +8,7 @@
package routers package routers
import ( import (
"net/http"
"oc-scheduler/controllers" "oc-scheduler/controllers"
beego "github.com/beego/beego/v2/server/web" beego "github.com/beego/beego/v2/server/web"
@@ -41,4 +42,7 @@ func init() {
) )
beego.AddNamespace(ns) beego.AddNamespace(ns)
// Route WebSocket hors du pipeline Beego pour éviter le WriteHeader parasite
beego.Handler("/oc/:id/check", http.HandlerFunc(controllers.CheckStreamHandler))
} }

View File

@@ -15,6 +15,116 @@
}, },
"basePath": "/oc/", "basePath": "/oc/",
"paths": { "paths": {
"/booking/": {
"get": {
"tags": [
"booking"
],
"description": "find booking by id\n\u003cbr\u003e",
"operationId": "BookingController.GetAll",
"parameters": [
{
"in": "query",
"name": "is_draft",
"description": "draft wished",
"type": "string"
}
],
"responses": {
"200": {
"description": "{booking} models.booking"
}
}
}
},
"/booking/search/execution/{id}": {
"get": {
"tags": [
"booking"
],
"description": "search bookings by execution\n\u003cbr\u003e",
"operationId": "BookingController.Search",
"parameters": [
{
"in": "path",
"name": "id",
"description": "id execution",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "is_draft",
"description": "draft wished",
"type": "string"
}
],
"responses": {
"200": {
"description": "{workspace} models.workspace"
}
}
}
},
"/booking/search/{start_date}/{end_date}": {
"get": {
"tags": [
"booking"
],
"description": "search bookings\n\u003cbr\u003e",
"operationId": "BookingController.Search",
"parameters": [
{
"in": "path",
"name": "start_date",
"description": "the word search you want to get",
"required": true,
"type": "string"
},
{
"in": "path",
"name": "end_date",
"description": "the word search you want to get",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "is_draft",
"description": "draft wished",
"type": "string"
}
],
"responses": {
"200": {
"description": "{workspace} models.workspace"
}
}
}
},
"/booking/{id}": {
"get": {
"tags": [
"booking"
],
"description": "find booking by id\n\u003cbr\u003e",
"operationId": "BookingController.Get",
"parameters": [
{
"in": "path",
"name": "id",
"description": "the id you want to get",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "{booking} models.booking"
}
}
}
},
"/execution/": { "/execution/": {
"get": { "get": {
"tags": [ "tags": [
@@ -240,6 +350,41 @@
} }
} }
}, },
"/{id}/check": {
"get": {
"tags": [
"oc-scheduler/controllersWorkflowSchedulerController"
],
"description": "WebSocket stream of slot availability for a workflow.\n\u003cbr\u003e",
"operationId": "WorkflowSchedulerController.CheckStream",
"parameters": [
{
"in": "path",
"name": "id",
"description": "workflow id",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "as_possible",
"description": "find nearest free slot from now",
"type": "boolean"
},
{
"in": "query",
"name": "preemption",
"description": "validate anyway, raise warnings",
"type": "boolean"
}
],
"responses": {
"101": {
"description": ""
}
}
}
},
"/{id}/order": { "/{id}/order": {
"get": { "get": {
"tags": [ "tags": [
@@ -279,6 +424,10 @@
"name": "loki", "name": "loki",
"description": "Operations about workflow\n" "description": "Operations about workflow\n"
}, },
{
"name": "booking",
"description": "Operations about workspace\n"
},
{ {
"name": "execution", "name": "execution",
"description": "Operations about workflow\n" "description": "Operations about workflow\n"

View File

@@ -57,6 +57,31 @@ paths:
responses: responses:
"200": "200":
description: '{workspace} models.workspace' description: '{workspace} models.workspace'
/{id}/check:
get:
tags:
- oc-scheduler/controllersWorkflowSchedulerController
description: |-
WebSocket stream of slot availability for a workflow.
<br>
operationId: WorkflowSchedulerController.CheckStream
parameters:
- in: path
name: id
description: workflow id
required: true
type: string
- in: query
name: as_possible
description: find nearest free slot from now
type: boolean
- in: query
name: preemption
description: validate anyway, raise warnings
type: boolean
responses:
"101":
description: ""
/{id}/order: /{id}/order:
get: get:
tags: tags:
@@ -74,6 +99,86 @@ paths:
responses: responses:
"200": "200":
description: '{workspace} models.workspace' description: '{workspace} models.workspace'
/booking/:
get:
tags:
- booking
description: |-
find booking by id
<br>
operationId: BookingController.GetAll
parameters:
- in: query
name: is_draft
description: draft wished
type: string
responses:
"200":
description: '{booking} models.booking'
/booking/{id}:
get:
tags:
- booking
description: |-
find booking by id
<br>
operationId: BookingController.Get
parameters:
- in: path
name: id
description: the id you want to get
required: true
type: string
responses:
"200":
description: '{booking} models.booking'
/booking/search/{start_date}/{end_date}:
get:
tags:
- booking
description: |-
search bookings
<br>
operationId: BookingController.Search
parameters:
- in: path
name: start_date
description: the word search you want to get
required: true
type: string
- in: path
name: end_date
description: the word search you want to get
required: true
type: string
- in: query
name: is_draft
description: draft wished
type: string
responses:
"200":
description: '{workspace} models.workspace'
/booking/search/execution/{id}:
get:
tags:
- booking
description: |-
search bookings by execution
<br>
operationId: BookingController.Search
parameters:
- in: path
name: id
description: id execution
required: true
type: string
- in: query
name: is_draft
description: draft wished
type: string
responses:
"200":
description: '{workspace} models.workspace'
/execution/: /execution/:
get: get:
tags: tags:
@@ -205,6 +310,9 @@ tags:
- name: loki - name: loki
description: | description: |
Operations about workflow Operations about workflow
- name: booking
description: |
Operations about workspace
- name: execution - name: execution
description: | description: |
Operations about workflow Operations about workflow

115
ws.go Normal file
View File

@@ -0,0 +1,115 @@
//go:build ignore
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/signal"
"time"
"golang.org/x/net/websocket"
)
func main() {
timeout := flag.Int("timeout", 30, "secondes sans message avant de quitter")
flag.Parse()
args := flag.Args()
// Exemples de routes WS disponibles :
// ws://localhost:8090/oc/<workflow-id>/check
// ws://localhost:8090/oc/<workflow-id>/check?as_possible=true
// ws://localhost:8090/oc/<workflow-id>/check?as_possible=true&preemption=true
url := "ws://localhost:8090/oc/WORKFLOW_ID/check?as_possible=true"
token := ""
// Body JSON envoyé comme premier message WebSocket (WorkflowSchedule).
// Seuls start + duration_s sont requis si as_possible=true.
body := `{"start":"` + time.Now().UTC().Format(time.RFC3339) + `","duration_s":3600}`
if len(args) >= 1 {
url = args[0]
}
if len(args) >= 2 {
token = args[1]
}
if len(args) >= 3 {
body = args[2]
}
origin := "http://localhost/"
config, err := websocket.NewConfig(url, origin)
if err != nil {
log.Fatalf("Config invalide : %v", err)
}
if token != "" {
config.Header.Set("Authorization", "Bearer "+token)
fmt.Printf("Token : %s...\n", token[:min(20, len(token))])
}
fmt.Printf("Connexion à : %s\n", url)
ws, err := websocket.DialConfig(config)
if err != nil {
log.Fatalf("Impossible de se connecter : %v", err)
}
defer ws.Close()
fmt.Println("Connecté — envoi du body initial...")
// Envoi du WorkflowSchedule comme premier message.
if err := websocket.Message.Send(ws, body); err != nil {
log.Fatalf("Impossible d'envoyer le body initial : %v", err)
}
fmt.Printf("Body envoyé : %s\n\nEn attente de messages...\n\n", body)
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
msgs := make(chan string)
errs := make(chan error, 1)
go func() {
for {
var raw string
if err := websocket.Message.Receive(ws, &raw); err != nil {
errs <- err
return
}
msgs <- raw
}
}()
idleTimer := time.NewTimer(time.Duration(*timeout) * time.Second)
defer idleTimer.Stop()
for {
select {
case <-stop:
fmt.Println("\nInterruption — fermeture.")
return
case err := <-errs:
fmt.Printf("Connexion fermée : %v\n", err)
return
case <-idleTimer.C:
fmt.Printf("Timeout (%ds) — aucun message reçu, fermeture.\n", *timeout)
return
case raw := <-msgs:
idleTimer.Reset(time.Duration(*timeout) * time.Second)
var data any
if err := json.Unmarshal([]byte(raw), &data); err == nil {
b, _ := json.MarshalIndent(data, "", " ")
fmt.Println(string(b))
} else {
fmt.Printf("Message brut : %s\n", raw)
}
}
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}