Stream negotiated canonical model reads with snapshot framing
Foundry Go checks / check (push) Failing after 10m37s
Foundry Go checks / check (pull_request) Failing after 10m45s

This commit is contained in:
2026-11-09 02:01:46 +08:00
parent ff88865e91
commit e38254821a
4 changed files with 198 additions and 0 deletions
+5
View File
@@ -172,6 +172,11 @@ func (s *service) getCanvasModel(w http.ResponseWriter, r *http.Request, project
if err = tx.Rollback(); err != nil {
return err
}
if r.Header.Get("Accept") == modelStreamType {
writeCanvasModelStream(r.Context(), w, model)
return nil
}
w.Header().Add("Vary", "Accept")
reply(w, 200, response{"model": model.view(nil)})
return nil
}
@@ -214,6 +214,16 @@ func TestCanonicalModelMigrationAndIncrementalLifecycle(t *testing.T) {
if call("GET", base+"/model", nil, "", 200)["model"].(map[string]any)["complete"] != true {
t.Fatal("initial read has no explicit coverage")
}
// Negotiate the same authenticated canonical read as a streamed transfer.
streamRequest := httptest.NewRequest("GET", base+"/model", nil)
streamRequest.Header.Set("Authorization", "Bearer "+token)
streamRequest.Header.Set("Accept", modelStreamType)
streamWriter := httptest.NewRecorder()
mux.ServeHTTP(streamWriter, streamRequest)
streamed := streamRecords(t, streamWriter)
if streamWriter.Code != 200 || streamWriter.Header().Get("Content-Type") != modelStreamType || len(streamed) < 2 || streamed[0]["projectId"] != root || streamed[len(streamed)-1]["kind"] != "complete" || !modelEqual(streamed[0]["versions"], read().Versions) {
t.Fatal("authenticated stream lost canonical snapshot coverage")
}
// An object created and removed inside one transaction still has a lifecycle fact.
ghostID := modelTestID(4000)
ghost := modelTestNode(ghostID, root)
+91
View File
@@ -0,0 +1,91 @@
package platform
import (
"context"
"encoding/json"
"net/http"
)
const modelStreamType = "application/vnd.foundry.canvas-model+ndjson;version=1"
// The caller captures a coherent model while holding the workspace lock, then
// releases its transaction before writing. A slow reader must not hold a writer.
// This streams transfer/encoding; it does not stream database row acquisition.
func writeCanvasModelStream(ctx context.Context, w http.ResponseWriter, model canvasModel) {
token := newModelID()
if token == "" {
reject(w, 500, "model_stream_unavailable")
return
}
view := model.view(nil)
header := response{"streamVersion": 1, "kind": "header", "sequence": 0, "snapshotToken": token,
"documentFormatVersion": canvasModelFormat, "projectId": model.Root, "canvases": view.Canvases,
"versions": view.Versions, "tombstones": view.Tombstones, "removedCanvasIds": view.Removed, "elementCount": len(view.Elements)}
const maxLine = 8 * 1024 * 1024
const maxBytes = 64 * 1024 * 1024
sequence, total := 0, 0
started := false
write := func(value response) bool {
if ctx.Err() != nil {
return false
}
raw, err := json.Marshal(value)
if err != nil || len(raw) > maxLine || total+len(raw)+1 > maxBytes || sequence >= 4096 {
if !started {
reject(w, 413, "model_stream_budget")
}
return false
}
if !started {
w.Header().Set("Content-Type", modelStreamType)
w.Header().Set("Cache-Control", "no-store")
w.Header().Add("Vary", "Accept")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
started = true
}
total += len(raw) + 1
if n, writeErr := w.Write(append(raw, '\n')); writeErr != nil || n != len(raw)+1 {
return false
}
sequence++
// A missing terminal record is an explicit incomplete stream at the client.
// Never append an unrelated JSON error after streaming has begun.
return http.NewResponseController(w).Flush() == nil
}
if !write(header) {
return
}
batch := []map[string]any{}
batchBytes := 0
flush := func() bool {
if len(batch) == 0 {
return true
}
if !write(response{"streamVersion": 1, "kind": "elements", "sequence": sequence, "snapshotToken": token, "elements": batch}) {
return false
}
batch, batchBytes = []map[string]any{}, 0
return true
}
for _, element := range view.Elements {
if ctx.Err() != nil {
return
}
raw, err := json.Marshal(element)
if err != nil {
return
}
if len(batch) > 0 && (len(batch) == 64 || batchBytes+len(raw) > 512*1024) {
if !flush() {
return
}
}
batch = append(batch, element)
batchBytes += len(raw)
}
if !flush() {
return
}
write(response{"streamVersion": 1, "kind": "complete", "sequence": sequence, "snapshotToken": token, "elementCount": len(view.Elements)})
}
@@ -0,0 +1,92 @@
package platform
import (
"bufio"
"context"
"encoding/json"
"fmt"
"net/http/httptest"
"testing"
)
func streamRecords(t *testing.T, writer *httptest.ResponseRecorder) []response {
t.Helper()
var records []response
scanner := bufio.NewScanner(writer.Body)
for scanner.Scan() {
var record response
if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
t.Fatal(err)
}
records = append(records, record)
}
if err := scanner.Err(); err != nil {
t.Fatal(err)
}
return records
}
func TestCanonicalModelStreamFramesAndSnapshot(t *testing.T) {
model := modelTestDocument()
for i := 0; i < 129; i++ {
id := fmt.Sprintf("extra-%03d", i)
model.Elements[id] = modelTestNode(id, modelRootID)
}
model.Tombstones["deleted"] = 2
model.TombstoneOwners["deleted"] = modelRootID
writer := httptest.NewRecorder()
writeCanvasModelStream(context.Background(), writer, model)
if writer.Code != 200 || writer.Header().Get("Content-Type") != modelStreamType || !writer.Flushed {
t.Fatal("stream was not negotiated/flushed", writer.Code, writer.Header())
}
records := streamRecords(t, writer)
if len(records) != 5 || records[0]["kind"] != "header" || records[4]["kind"] != "complete" {
t.Fatal("missing stream framing", len(records))
}
token := modelString(records[0]["snapshotToken"])
if !uuidSlugPattern.MatchString(token) || records[0]["projectId"] != modelRootID || !modelEqual(records[0]["versions"], model.Versions) || !modelEqual(records[0]["tombstones"], model.Tombstones) {
t.Fatal("stream header lost snapshot identity")
}
seen := map[string]bool{}
for i, record := range records {
if record["sequence"] != float64(i) || record["snapshotToken"] != token || record["streamVersion"] != float64(1) {
t.Fatal("mixed or unordered snapshot")
}
if record["kind"] == "elements" {
items := record["elements"].([]any)
if len(items) == 0 || len(items) > 64 {
t.Fatal("unbounded record batch")
}
for _, item := range items {
element := item.(map[string]any)
id := modelString(element["id"])
if seen[id] || !modelEqual(element, model.Elements[id]) {
t.Fatal("duplicated or corrupted stream element")
}
seen[id] = true
}
}
}
if len(seen) != len(model.Elements) || records[4]["elementCount"] != float64(len(seen)) {
t.Fatal("terminal coverage mismatch")
}
}
type cancelStreamWriter struct {
*httptest.ResponseRecorder
cancel context.CancelFunc
}
func (writer cancelStreamWriter) Flush() {
writer.ResponseRecorder.Flush()
writer.cancel()
}
func TestCanonicalModelStreamCancellationNeverClaimsCompletion(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
writer := httptest.NewRecorder()
writeCanvasModelStream(ctx, cancelStreamWriter{writer, cancel}, modelTestDocument())
records := streamRecords(t, writer)
if len(records) != 1 || records[0]["kind"] != "header" {
t.Fatal("cancelled stream wrote a tail", len(records))
}
}