Skip to content

feat(server): custom routes, watch toggle, embedding API across all three engines - #170

Merged
Liam0205 merged 21 commits into
masterfrom
feat/169-runtime-serverplus
Jul 17, 2026
Merged

feat(server): custom routes, watch toggle, embedding API across all three engines#170
Liam0205 merged 21 commits into
masterfrom
feat/169-runtime-serverplus

Conversation

@Liam0205

Copy link
Copy Markdown
Owner

Closes #169

Summary

Upstreams the downstream serverplus package as a strict superset of the existing server, across all three engines (pine-go / pine-java / pine-cpp):

  • Custom routesRoute{Method, Path, Ingress, Egress}: Ingress converts the raw HTTP request into an engine request, Egress owns the entire HTTP response. Startup validation rejects built-in endpoint conflicts, duplicates, malformed paths and nil adapters, with byte-identical error strings across engines.
  • Watch toggle — config hot-reload watcher can now be disabled (Config.Watch in Go, setWatch in Java, ServerConfig.watch in C++). Default stays enabled for backward compatibility.
  • Embedding API (Go, Java) — for embedding the engine into existing HTTP frameworks (e.g. Gin) without the built-in server:
    • Go: NewServer(cfg) + Server.Execute(ctx, req) + Server.Acquire() *Handle + Server.Close(); Run() is reimplemented on top. All failure paths return errors instead of log.Fatal.
    • Java: load() + execute(common, items) + acquire() → Handle on PineServer.
    • C++: intentionally no embedding API — the C++ server is not a public library API (lives in src/, not include/pine/); it keeps the existing shared_mutex lifecycle and gets Route + Watch black-box parity only.
  • Bounded metrics labels — custom route paths join the known-path set, so they are labeled by their own path in pine_http_requests_total / /stats.http while unknown paths still collapse to _other.

Bug fix (found during parity work)

PineServer.stop() released the baseline snapshot but never cleared the snapshot reference. Any later acquireSnapshot() would spin forever on the retired snapshot (refs=0, CAS acquire always fails) instead of reporting engine-not-loaded. Now getAndSet(null), matching Go Close()'s Swap(nil).

Testing

  • Go: routes_test.go — validateRoutes matrix, Watch toggle, Execute/Acquire/Close lifecycle, routeHandler 405/ingress-error/execute paths. Full go test ./... green.
  • Java: RouteTest.java — 21 cases mirroring the Go suite. Full suite 290 tests green.
  • C++: test_routes.cpp — 10 doctest cases pinning validation error strings + normalize_path. WERROR build clean, ctest green.
  • Cross-validate: new section 20 (custom routes parity) runs all three servers with -demo-routes -watch=false and checks: echo 200 body parity, wrong-method 405, ingress-error 400, built-ins unaffected, unknown 404, custom path metrics label, watch=false keeps reload_count at 0. Full 20-section run green (55 PASS), including byte-exact /execute and server-http sections.
  • Fuzz: make fuzz (Go 30s + Jazzer) clean.

Compatibility

Zero custom routes + default Watch → behavior is byte-identical to the previous server. Run(cfg) signature unchanged; Java/C++ CLI entrypoints unchanged unless the new flags/properties are passed.

Liam0205 added 6 commits July 17, 2026 18:12
Upstream the downstream serverplus package into pkg/server as a strict
superset of the existing server:

- Config.Routes: custom endpoints with Ingress (HTTP -> pine.Request)
  and Egress (result -> HTTP response) adapters; validated against
  built-in endpoint conflicts, duplicates, malformed paths, and nil
  adapters. Custom paths join the bounded known-path set for HTTP
  metrics (no cardinality explosion).
- Config.Watch (*bool): nil/true keeps the config hot-reload watcher
  (backward compatible); false disables it. pine.Bool helper added.
- Embedding API: NewServer(cfg) constructs the engine + resources +
  snapshot without net/http; Server.Execute runs a request against the
  live snapshot with in-flight ref held; Server.Acquire returns a
  Handle (Engine/Resources/ResourceMetrics + Release) for frameworks
  like Gin; Server.Close joins the watcher then drops the baseline ref.
- Run() reimplemented on top of NewServer; NewServer returns errors
  instead of log.Fatal.

Unit tests cover route validation, Watch toggle, Execute/Acquire/Close
lifecycle, and the routeHandler 405/ingress-error/execute paths.
-watch=false disables the config hot-reload watcher (Config.Watch).
-demo-routes registers POST /api/echo, a demo custom route used by
cross-validate to exercise Route/Ingress/Egress behavior across the
three engines. Smoke-verified: 200 on valid body, 400 on malformed
body, 405 on wrong method, custom path appears under its own label
in /stats http metrics (not _other).
Section 20 starts all three servers with -demo-routes -watch=false and
verifies: (1) POST /api/echo 200 + body parity, (2) wrong method 405,
(3) malformed body -> Ingress error -> Egress 400, (4) built-in
endpoints unaffected, (5) unknown path still 404, (6) custom route
path labeled by itself in /stats http metrics (bounded set), (7)
watch=false keeps reload_count at 0 after a config touch.

Docs: README core features + cross-validate list (19 -> 20 sections);
llmdoc dag-engine.md Server lifecycle section rewritten for
NewServer/Execute/Acquire/Close + Routes + Watch; metrics doc notes
custom paths join the bounded known-path set.
Port the Go Route/Ingress/Egress extension point to pine-cpp with
byte-identical black-box behavior:

- Route{method, path, ingress, egress} with RouteRequest/RouteResponse;
  Ingress converts the raw HTTP request into a pine::Request, Egress
  stages the full response (status/content-type/body).
- validate_routes (new routes.cpp, socket-free so it links into the
  test target) rejects built-in conflicts, duplicates, malformed paths
  and empty adapters with error strings byte-aligned to Go's
  validateRoutes.
- normalize_path now takes the dynamic known-path set (built-ins +
  custom routes) so custom paths get their own bounded metrics label.
- ServerConfig.watch (default true); false skips the config watcher
  thread. -watch=true|false and -demo-routes (POST /api/echo) flags
  wired into pineapple-server main.
- Custom-route execution holds the engine shared_lock for the whole
  ingress-result window (equivalent to Go's refcounted snapshot; the
  existing shared_mutex lifecycle is intentionally kept, no embedding
  API since the C++ server is not a public library).

tests/test_routes.cpp: 10 doctest cases pinning validation error
strings and normalize_path behavior. WERROR build clean, ctest green,
demo route output verified byte-exact vs pine-go.
Port the Go Route/Ingress/Egress extension point to PineServer:

- Static nested types Ingress (HttpExchange -> ExecRequest), Egress
  (writes the full response), ExecRequest, Route. addRoute validates
  before start() with error strings byte-aligned to Go's
  validateRoutes (built-in conflicts, duplicates, malformed paths,
  null adapters).
- setWatch(false) disables the config hot-reload watcher; load() is
  the embedding entrypoint (config parse + loadConfig + optional
  watcher, idempotent) and start() builds the HTTP layer on top of it.
- Embedding API: execute(common, items) runs against the refcounted
  live snapshot (throws IllegalStateException("engine not loaded")
  when none); acquire() returns a Handle exposing
  engine()/resources()/resourceMetrics()/release().
- Custom routes register through wrapHandler with the route path as
  its own bounded metrics label; routeHandler enforces method (405),
  routes ingress failures to egress(null, e).
- main() wires -Dpine.watch and -Dpine.demoRoutes (POST /api/echo,
  byte-exact with the Go/C++ demo route via sortMapKeys).

Fix: stop() now clears the snapshot reference (getAndSet(null),
matching Go Close()'s Swap(nil)). Previously acquireSnapshot() after
stop() would spin forever on the retired snapshot (refs=0, acquire
always false) instead of reporting engine-not-loaded.

RouteTest.java adds 21 cases mirroring pine-go routes_test.go. Full
suite: 290 tests green. Demo route smoke-verified byte-exact vs Go.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 审查

项目 结果
结论 ✅ APPROVE
审查截止 a6e20095743f8a44b61593bd5e855707a51a1adf

代码质量高,三引擎(pine-go / pine-java / pine-cpp)的自定义路由、Watch 开关、嵌入 API 行为对齐良好,测试覆盖充分(Go/Java 单测 + C++ doctest + cross-validate section 20 + fuzz)。核心并发语义(引用计数快照 + CAS acquire)在三端一致,未发现阻塞问题。

逐项确认的关键点:

  • Java stop() bug fix 正确snapshot.getAndSet(null) 清空引用后,acquireSnapshot()snap == null 分支直接返回 null,不再对已退休快照(refs=0,acquire() 恒为 false)自旋。与 Go Close()Swap(nil) 语义对齐。
  • Go 构造/传输分离NewServers.metricsProviderserver.go:312),Run 在其上组装 mux 并复用(server.go:229);validateRoutesmux.Handle 注册前执行,重复/冲突路径不会触发 ServeMux panic;失败路径由 defer s.Close() 兜底清理。
  • C++ 生命周期安全route_map_ 存的是指向 config_.routes 的指针,config_ = cfgserver.cpp:1168)在建 map 之前完成拷贝,二者同为 Server 成员,指针始终有效;RouteResponse resp 声明在 arena scope 之外,egress 写入的 body 用默认分配器,arena 析构后仍有效;watcher_thread.joinable() 守护 watch=false 下的 join。
  • 错误文案字节对齐 — 三端 validate 的错误串(含 Go %q 的引号包裹)逐字一致,cross-validate section 20 对 echo/405/400/404/metrics label/watch 做了黑盒比对。

🟢 小问题 (1)

  • 文件: pine-java/src/main/java/page/liam/pine/PineServer.java start()
  • 问题: start() 先调用 load()(启动 watcher 线程 + 建立 snapshot baseline),再执行 HttpServer.create(...)。若后者抛异常(例如端口已占用),watcher executor 与 snapshot 引用不会被回收,除非调用方随后显式调用 stop()。Go 侧 Rundefer s.Close() 覆盖了这一路径,Java/嵌入场景没有对等兜底。
  • 影响: 仅影响 start() 部分失败的边界;main() 因进程退出不受影响。非阻塞。
  • 建议: 可在 start()load() 之后用 try/catch,失败时调用 stop() 清理,与 Go 的 defer Close() 对齐(可选改进)。

…iew)

If HttpServer.create or context registration throws after load()
succeeded (e.g. port already bound), the watcher thread and snapshot
baseline used to leak unless the caller remembered to call stop().
start() now wraps the HTTP-layer setup in try/catch and calls stop()
on failure, mirroring Go Run()'s defer Close. New RouteTest case pins
the rollback: execute() reports engine-not-loaded and acquire()
returns null after a failed start().
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 a6e20095743f8a44b61593bd5e855707a51a1adf..603238badc53c94d41f85e608a7518517aac0426
审查截止 603238badc53c94d41f85e608a7518517aac0426

增量仅改动两个 Java 文件,正是上次审查中「🟢 小问题」指出的 start() 部分失败回滚问题。修复正确、有对应测试,无新增问题。

逐项确认:

  • 回滚逻辑正确start()load() 之后将 HTTP 层搭建包进 try/catch,任意一行抛异常(HttpServer.create 端口占用、createContexthttpServer.start())都会走 catch 调用 stop() 再 rethrow,与 Go Run()defer Close 对齐(PineServer.java:204-229)。
  • stop() 对部分初始化状态安全watcherExecutor / httpServer / httpExecutor 均有 null 守护,snapshot.getAndSet(null) 亦 null 安全(PineServer.java:541-561)。因此无论 create 前(httpServer 仍为 null)还是 createContext/start 后抛异常,回滚都不会 NPE,且都会清空快照并停掉 watcher 线程。
  • 测试到位testStartFailureRollsBackLoadServerSocket 占端口触发 start() 失败,断言 execute()engine not loadedacquire() 返回 null,精准锁定回滚语义(RouteTest.java:181-197);ServerSocket 与断言方法 import 均已就绪。

上次审查的三引擎行为对齐、并发语义、错误文案字节对齐等结论不受本次增量影响,继续保持。至此 issue #169 的三端实现与我提出的唯一改进点均已闭环。

Reflection upstream-serverplus-custom-routes.md captures: the
demo-routes black-box validation pattern for programmatic extension
points, error-string lock-in at design time, the intentional C++
no-embedding-API decision, the accepted Java throw/return error-model
asymmetry, plus process lessons (worktree patch collection, old state
machines reached through new entrypoints, mvn diagnostics pitfalls).

Stable-doc promotions: cross-layer-validation.md gains the demo-routes
pattern; pine-cpp-runtime.md documents the behavior-parity /
structure-freedom decision; dag-engine.md archives the Java error-model
divergence; standard-workflow.md adds the parallel-worker change
collection protocol. index.md synced.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 603238badc53c94d41f85e608a7518517aac0426..37fa01d0ea2eb958da4baaead02c02cfe61bc079
审查截止 37fa01d0ea2eb958da4baaead02c02cfe61bc079

增量仅为文档改动,无任何代码变更。新增/修订的都是 llmdoc/ 内部知识文档,内容准确反映了此前已审查通过的三引擎实现,无问题。

逐项确认:

  • 改动范围纯文档git diff 仅触及 6 个 llmdoc/ 文件(architecture/dag-engine.mdarchitecture/pine-cpp-runtime.mdguides/cross-layer-validation.mdguides/standard-workflow.mdindex.md、新增 memory/reflections/upstream-serverplus-custom-routes.md),共 +75/-4 行,无源码、无测试、无脚本变更。
  • 文档与代码一致 — dag-engine.md 记录的「Java routeHandler 抛/返回二分 vs Go 统一 err 传 Egress」、pine-cpp-runtime.md 的「C++ 黑盒行为对等、不做嵌入 API、validate_routes/normalize_path 抽到 socket-free routes.cpp」均与前两次审查确认的实现语义吻合,无夸大或失实。
  • index.md 索引同步 — 三处条目描述与对应文档正文的新增段落一一对应,未出现悬空引用。
  • reflection 文档 — 新增复盘记录了 demo-routes 黑盒验证、错误文案设计期锁定、C++ 无嵌入 API、Java 错误模型二分四项决策及三条过程教训,属知识沉淀,不影响运行时行为。

前两次审查关于三引擎行为对齐、并发语义(引用计数快照 + CAS acquire)、stop() bug fix、start() 失败回滚、错误文案字节对齐的结论均不受本次文档增量影响,继续保持。issue #169 至此代码与文档均已闭环。

Liam0205 added 4 commits July 17, 2026 21:46
…llback (review)

Address the from-c353c04 code-review findings on the Go side:

- Custom routes now apply MaxRequestBodySize via http.MaxBytesReader
  before user Ingress code reads the body, and a tripped cap responds
  413 centrally (same bytes as /execute) without invoking Egress —
  custom endpoints can no longer bypass the server-wide limit. The
  demo Ingress propagates read errors as-is so MaxBytesError reaches
  the central handler.
- Custom route paths dispatch via exact string lookup behind the "/"
  fallback instead of ServeMux pattern registration: a trailing slash
  no longer becomes a subtree wildcard, "{name}" is a literal, and
  malformed patterns cannot panic mux setup. Matches pine-java's
  exact-path guard and pine-cpp's map lookup.
- normalizeConfig applies the :8080 default in both Run and NewServer;
  previously NewServer defaulted only its own copy and Run handed the
  original empty Addr to http.Server (net/http would bind :http/80).
- NewServer now rolls back on every failure path between engine
  creation and snapshot publication (deferred engine.Close + rm.Stop),
  and Manager.Start releases values loaded before a mid-load failure
  so partially-initialized resources cannot leak open connections.

Tests: 413-on-custom-route (Egress not called), exact-path dispatch
matrix (trailing slash, braces, deep paths), normalizeConfig defaults,
and a rollback test pinning that a Closer resource loaded before a
failing one is closed when NewServer fails.
- routeHandler wraps the exchange body in LimitedBodyStream before
  user Ingress code reads it: byte limit+1 throws
  BodyLimitExceededException, which the handler maps to the same
  central 413 response as /execute (Egress never runs). Custom routes
  can no longer bypass max_request_body_size.
- stop() now awaits watcher termination (shutdownNow only interrupts,
  and loadConfig never polls the interrupt flag), and snapshot
  publication happens under a stop lock with a closed flag: a reload
  that already passed the mtime check when stop() ran can no longer
  re-publish a snapshot after teardown — previously that leaked the
  new snapshot and let execute()/acquire() spuriously come back to
  life. Go's Close() (watcher join, then Swap(nil)) is the reference
  semantics.

RouteTest gains a reload-after-stop case pinning that a late
loadConfig refuses to publish and the server stays engine-not-loaded.
Full suite: 291 tests green.
…e (review)

Custom routes previously called engine_->execute directly, bypassing
the shared server execution path: scheduler run_count and per-operator
exec/skip stats never counted custom-route pipeline runs, diverging
from Go/Java /stats. handle_custom_route now goes through
execute_with_trace — same stats accounting, same lock discipline, and
client-disconnect cancel now works on custom routes exactly like
/execute. Engine-not-loaded still surfaces to Egress as an error.
…y, C++ arm gate (review)

- New check [7]: POST /api/echo must bump scheduler run_count and
  operator exec_count on all three engines — guards the C++ regression
  where custom routes bypassed the shared execution path and /stats
  stayed flat.
- New check [8]: an oversized body on the custom route returns the
  central 413 with byte-identical body across engines.
- CPP_SERVER set but readiness failed is now a hard fail (previously
  the C++ arm silently vanished with zero checks and the section still
  passed), plus a terminal assertion that a configured C++ binary ran
  a non-zero number of checks.
- README and make help drop hard-coded section counts per the
  llmdoc/must/conventions.md rule; both point at scripts/cross-validate/
  as the source of truth.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 37fa01d0ea2eb958da4baaead02c02cfe61bc079..54922de8d1b76346bb3c98aaad0e51436fa35261
审查截止 54922de8d1b76346bb3c98aaad0e51436fa35261

本次增量是对前一轮 code-review 发现的集中修复,四个提交分别加固了 Go/Java/C++ 三端的自定义路由,并同步收紧了 cross-validate section 20。逐项验证正确,测试到位,未发现阻塞或重要问题。

逐项确认:

  • Go:自定义路由不再绕过 body 上限routeHandler 在 Ingress 前用 http.MaxBytesReader 包裹 body,errors.As(err, &maxErr) 命中时集中回 413(与 /execute 同字节),不进 Egress(server.go:622-639);demo Ingress 改为原样透传 read error 以让 MaxBytesError 抵达中心处理(main.go:29-32)。
  • Go:精确路径分发 — 自定义路由改走 fallbackHandler 的精确 map 查找而非 mux.Handle 模式注册,尾斜杠不再成子树通配、{name} 不被解释、非法模式不会 panic mux(server.go:300-312),与 pine-java 路径守卫、pine-cpp route_map_ 查找语义对齐。
  • Go::8080 默认值统一normalizeConfig 抽出后 RunNewServer 同时调用,空 Addr 不再落到 net/http 的 :http(80 端口)(server.go:290-296)。
  • Go:初始化失败回滚NewServerpublished 标志 + defer 覆盖 engine 创建到快照发布之间的所有失败路径(engine.Close + rm.Stop)(server.go:369-387);Manager.Start 内部亦对已加载值做释放且保持 started=false,故 rm.Stop() 二次调用为 no-op,无重复释放(resource.go:282-301)。
  • Java:body 上限 + 边界语义LimitedBodyStream 在 Ingress 前经 setStreams 注入,恰好 limit 字节放行、limit+1BodyLimitExceededException → 413,与 Go MaxBytesReader 边界一致;setStreams(in, null) 保留原输出流,用法正确(PineServer.java:460-536)。
  • Java:stop/reload 竞态stop()awaitTermination 等待在途 reload 结束,再在 stopLock 下置 closedgetAndSet(null)loadConfig 发布前在同锁下检查 closed,落后的 reload 会释放新快照并抛异常,不再复活已停服务器(PineServer.java:601-676)。
  • C++:自定义路由走 execute_with_trace — 不再直接调 engine_->execute,改与 /execute 同一路径,scheduler run_count、算子 exec/skip 统计与客户端断连取消对自定义路由同样生效(server.cpp:859-885);先在 shared_lock 下判 engine_loaded 再执行,与既有 /execute 的 check-then-execute 模式一致(且比 /execute 无锁读 engine_ 更谨慎),body 413 由 dispatch 前的中心 content-length 检查统一覆盖(server.cpp:1404-1409),故 C++ 自定义路由天然受限。
  • cross-validate section 20 加固 — 新增 check [7](/api/echo 必须 bump run_count+exec_count,防 C++ 回归)、check [8](超限 body 三端 413 + body 字节比对);CPP_SERVER 已配置但 readiness 失败改为硬失败,并加终态断言「配置了 C++ 二进制却跑了 0 项检查」即失败,堵住此前 C++ 臂静默消失仍判 pass 的漏洞(20-custom-routes.sh:64-75, 326-349);README/Makefile 按 llmdoc/must/conventions.md 去掉硬编码 section 数,指向 scripts/cross-validate/ 为准。
  • 测试覆盖 — Go 新增 4 个用例(body 413 不触发 Egress、精确路径分发矩阵、默认 Addr、资源回滚含 Closer 关闭信号);Java 新增 testReloadAfterStopDoesNotResurrectSnapshot 锁定落后 reload 拒绝发布。

前三轮审查关于三引擎行为对齐、并发语义(引用计数快照 + CAS acquire)、stop() bug fix、start() 失败回滚、错误文案字节对齐的结论均不受本次增量影响,继续保持。issue #169 的实现、加固与文档至此闭环。

注:本地未运行测试(命令需审批),结论基于代码走查,最终以 CI 为准。

Reflection gains a second-round section covering the 8 findings from
the local deep review (body-limit bypass, ServeMux pattern semantics,
by-value Config default regression, stop/reload race, C++ stats
bypass, fail-open test arm, hard-coded section counts, bot-APPROVE
!= no-issues).

Promotions: standard-workflow expands the state-machine entry to
"rescan the whole machine after fixing one hole" (reachability,
concurrency interleaving, publication order); cross-layer-validation
adds observable-side-effects parity (stats increments, check [7]) and
fail-closed optional test arms; dag-engine documents the shared
body-limit contract and exact-path fallback dispatch. index synced.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 54922de8d1b76346bb3c98aaad0e51436fa35261..ae8a2f564cb9025ddfc2b6c8deeacf873167b7d7
审查截止 ae8a2f564cb9025ddfc2b6c8deeacf873167b7d7

增量仅为一个纯文档提交(ae8a2f5),把第二轮深度 code-review 的经验教训沉淀到 llmdoc/。无任何源码、测试、脚本变更,内容与前一轮已审查通过的代码修复一致,无问题。

逐项确认:

  • 改动范围纯文档git diff --stat 仅触及 5 个 llmdoc/ 文件(architecture/dag-engine.mdguides/cross-layer-validation.mdguides/standard-workflow.mdindex.mdmemory/reflections/upstream-serverplus-custom-routes.md),共 +46/-5 行,无源码/测试/脚本变更。
  • 文档与代码一致 — 新增段落描述的事实均与上一轮(54922de)已确认的实现吻合:
    • dag-engine.md「请求体上限在共享分发层统一施加」对应 Go http.MaxBytesReader → 413、Java LimitedBodyStream、C++ socket 层集中限流(dag-engine.md:613);「自定义路由不注册为 ServeMux pattern 而走 / fallback 精确查表」对应 fallbackHandler + map lookup,「空 Addr 由 normalizeConfig 统一补 :8080」对应两入口共享归一化(dag-engine.md:605)——均为前一轮验证过的修复。
    • cross-layer-validation.md 新增「对等验证要覆盖执行路径可观测副作用」(对应 C++ execute_with_trace 旁路 + section 20 check [7])与「可选测试臂 fail-closed」(对应 CPP_SERVER 配置了但 readiness 失败硬失败 + cpp_cr_total > 0 终态断言),与脚本实现一一对应(cross-layer-validation.md:140-154)。
    • standard-workflow.md 把首轮「新入口重审旧状态机可达性」扩展为「修一个洞后重扫整个状态机:可达性 / 并发交错 / 发布顺序」,准确对应 Java stop() 自旋洞(首轮)与 stop/reload 竞态洞(二轮)两个窗口(standard-workflow.md:118-120)。
  • index.md 索引同步 — 三处条目描述(standard-workflow、cross-layer-validation、reflection)与对应文档正文的新增段落一致,无悬空引用(index.md:21-115)。
  • reflection 二轮小节 — 新增「第二轮:深度 code-review 修复」记录 8 项问题(阻塞 1 + 重要 6 + 小 1)及四个修复 commit 归属,属知识沉淀,不影响运行时行为(upstream-serverplus-custom-routes.md:55-76)。

前四轮审查关于三引擎行为对齐、并发语义(引用计数快照 + CAS acquire)、stop() bug fix、start() 失败回滚、body 上限、精确路径分发、C++ stats 对齐、错误文案字节对齐的结论均不受本次文档增量影响,继续保持。issue #169 的实现、加固与经验沉淀至此完整闭环。

Liam0205 added 4 commits July 17, 2026 23:36
The previous commit moved custom routes onto execute_with_trace but
left a check-then-execute race: the caller checked engine_ under a
short shared lock, released it, and execute_with_trace then read
resource_manager_ OUTSIDE any lock before re-acquiring engine_mu_ for
the execute. reload_config swaps BOTH engine_ and resource_manager_
under the exclusive lock and tears the old ones down after unlocking,
so a custom-route request interleaving with a hot reload could race
the unique_ptr (use-after-free) or pair an old resource snapshot with
a new engine.

execute_with_trace now does the engine-null check, the
resource_manager_->snapshot() and the execute inside ONE engine_mu_
shared-lock window, reporting an unloaded engine via the new
ExecuteResult.engine_not_loaded flag. The caller-side pre-check and
relock windows are gone: handle_execute maps the flag to the same 503
as before, custom routes forward it to Egress as "engine not loaded".
run_count now increments only when an engine actually executes.
FilterInputStream.skip delegates straight to the underlying stream, so
an Ingress calling skip()/skipNBytes() advanced past the body cap
unmetered — max_request_body_size could be bypassed by skipping the
request prefix instead of reading it. skip now counts against the
limit exactly like read, and both bulk read and skip are clamped to
remaining+1 so the underlying stream is never consumed far past the
cap. New RouteTest case: an Ingress that skipNBytes(1000) over a
16-byte cap gets the central 413 and Egress never runs.
…reload (review)

Restart the three servers with watch enabled and hammer /api/echo
while touching the config to force reloads: every response must stay
200 and reload_count must advance. Guards the C++ lock-window
regression (resource manager snapshotted outside the engine lock)
under real reload interleaving.
…(review)

- The "cp: same file" explanation claimed the error means identical
  content; GNU cp does not compare content — it means source and
  target resolve to the same file identity (same path or inode).
  Corrected in standard-workflow.md and the reflection, with guidance
  to diagnose via normalized paths / stat inode and to compare content
  explicitly with cmp/diff.
- Replace this round's hard-coded runtime-count wording (三引擎/三端)
  with 各运行时 or the explicit Go/Java/C++ list per
  llmdoc/must/conventions.md; also fixed the three-runtime comment in
  server.cpp.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 ae8a2f564cb9025ddfc2b6c8deeacf873167b7d7..b19163921f296468e15aaa869c319d5db4c49843
审查截止 b19163921f296468e15aaa869c319d5db4c49843

本次增量是对上一轮深度 review 遗留的两个并发/绕过洞的收尾修复:C++ 把引擎判空、资源快照、执行收进单一锁窗口,Java 给 LimitedBodyStream.skip() 计量,并补了一条热更新竞态的 cross-validate 检查。逐项走查正确,测试到位,未发现阻塞或重要问题。

逐项确认:

  • C++:单一锁窗口消除 check-then-execute 竞态execute_with_trace 现在把「引擎判空 + resource_manager_->snapshot() + execute_traced_into」全部收进同一个 engine_mu_ shared_lock 窗口(server.cpp:948-965)。此前资源快照在锁外读取,而 reload_config 在独占锁下同时换掉 engine_resource_manager_ 再于锁外析构旧对象,锁外读会与之竞争(unique_ptr 数据竞争 / 旧资源配新引擎)。修复正确。
  • C++:run_count_ 仅在真正执行时自增 — 从函数入口移入锁内 engine_ 非空分支(server.cpp:954),引擎未加载不再虚增计数,与 Go/Java /stats 语义一致。
  • C++:engine_not_loaded 标志的双路映射正确/execute 映射为 503(server.cpp:573-576),自定义路由通过 has_error 转 Egress error(镜像 Go ErrEngineNotLoaded 传 nil result)。early return 发生在 watcher joinwake_fd 关闭之后(server.cpp:977),无线程/fd 泄漏。handle_execute 入口保留的锁外快速判空(L483)仅为优化,权威判空已在锁内,安全。
  • Java:skip() 纳入 body 上限计量FilterInputStream.skip 原本直通底层流,Ingress 调 skipNBytes 可绕过 max_request_body_size;现 skip 与 bulk read 一致按 remaining+1 夹取并计数,越界抛 BodyLimitExceededExceptionPineServer.java:545-561)。单字节 read()L520)同样计量,无遗漏路径。边界语义(恰好 limit 放行、limit+1 抛)与 Go MaxBytesReader 对齐。
  • 测试 — Java 新增 testRouteHandlerBodyLimitNotBypassedBySkip:16 字节上限下 skipNBytes(1000) 应得 413 且 Egress 不执行,断言精准。
  • cross-validate check [10] — 新增热更新下的自定义路由竞态检查:watch=true 时反复 touch 配置并高频打 /api/echo,要求全程 200 且 reload_count >= 120-custom-routes.sh:334-396),专门守 C++ 锁窗口回归。C++ 臂沿用上一轮 fail-closed(配置了却未就绪即硬失败),PID 由 cr_cleanup 统一回收,无残留。
  • docs(b191639 — 纯文档 + 一处 server.cpp 注释措辞(three-runtimecross-runtime),按 llmdoc/must/conventions.md 去掉硬编码运行时计数,并订正「cp: same file」的解释(指文件身份而非内容相同)。不影响运行时行为。

前五轮审查关于三引擎行为对齐、引用计数快照 + CAS acquire 并发语义、stop() bug fix、start() 失败回滚、body 上限、精确路径分发、错误文案字节对齐的结论均不受本次增量影响,继续保持。issue #169 的实现、加固、竞态收尾与文档至此完整闭环。

注:本地未运行测试,结论基于代码走查,最终以 CI 为准。

Reflection gains a third-round section: the lock-window regression
introduced by the second-round fix itself (re-review your own diff
when changing lock structure), wrapper streams must meter every
advancing method (skip, not just read), documented tool-behavior
claims need verification before they land in llmdoc, and the
runtime-count literal rule needs to fire at writing time. index
synced.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 b19163921f296468e15aaa869c319d5db4c49843..0fb5d963196c1613686157419bc9ab847eb71ee5

本轮增量仅一个纯文档 commit(0fb5d96),无代码改动。

自上次审查截止点 b191639 以来,仅新增 llmdoc/memory/reflections/upstream-serverplus-custom-routes.md 的「第三轮:增量审查修复」小节,以及 llmdoc/index.md 对应索引条目的同步更新。

  • 复盘内容与前几轮代码修复(C++ 锁窗口回归 13adce9c、Java skip 计量 25c0b86a、cross-validate check [10] 6dfda6e0)一致,事实描述准确。
  • 无源代码变更,无需检查编译 / 逻辑 / 规范。

代码无问题,已检查增量改动。

…review)

The previous shape (touch, then a serial curl burst, then sleep) could
pass with zero overlap: the 2s watcher could reload during the sleep
after the burst finished, and all three servers shared one config file
so an earlier arm's touches pre-satisfied reload_count >= 1 for later
arms — the check never actually pinned the lock-window race it claims
to guard.

Now each arm: takes its own server's reload_count baseline, runs three
background workers hammering /api/echo continuously, touches a
per-server config file and polls until THIS server's counter strictly
increases past the baseline (20s deadline), keeps hammering briefly
past the observed reload, then stops workers and asserts zero non-200
responses. At least one reload demonstrably completes while requests
are in flight, and cross-arm counter pollution is impossible.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 0fb5d963196c1613686157419bc9ab847eb71ee5..6a91f1db4f8408d462742bbec835962f9111a043
审查截止 6a91f1db4f8408d462742bbec835962f9111a043

本轮增量仅一个纯测试 commit(6a91f1d),只改 scripts/cross-validate/20-custom-routes.sh(+80/-25),无源码变更。它重写了 check [10] 的热更新竞态测试,消除上一版「可能零重叠通过 + 跨臂计数污染」的缺陷。逐项走查正确,fail-closed 到位,无阻塞或重要问题。

逐项确认

  • 保证请求/重载真实重叠 — 每臂先取本服务器 reload_count 基线 rc0,启 3 个后台 worker 持续打 /api/echo,主线程反复 touch 配置并轮询直到 rc1 > rc0 严格增长(20s deadline)才停手。至少一次重载确实发生在请求在途期间,不再依赖「sleep 期间碰巧重载」的假设(20-custom-routes.sh:362-391)。
  • 消除跨臂计数污染 — Go/Java/C++ 各自 cp 出独立的 race 配置文件(race_config_{go,java,cpp}.json),一个臂的 touch 不再预满足另一个臂的 reload_countreload_race_arm 新增 $2=cfg 参数只碰自己的文件(20-custom-routes.sh:409-414:429-440)。
  • fail-closed 判定正确 — 通过条件是 reloaded && fails==0 && reqs>0:未观测到重载(deadline 到)判失败,任一非 200(含重载瞬间的连接失败/000)计入 fail_file 判失败,零请求判失败。这正是本检查要守的语义——重载不得丢请求(20-custom-routes.sh:401-405)。
  • worker 生命周期干净 — worker 循环判 stop_flag 文件,主线程观测到重载后 sleep 0.5 继续压测再 touch stop_flag,然后 wait "${worker_pids[@]}" 收敛;rm -f 在臂开始时清 stop/fail 文件,避免残留(20-custom-routes.sh:359:389-391)。
  • reload_count_of 容错 — 整条管线 curl | python3 2>/dev/null || echo 0,空响应/解析失败退化为 0,rc0/rc1 恒为数值,(( )) 比较安全(20-custom-routes.sh:346-348)。
  • shell 细节无误 — 函数内子 shell ( local n=0 code; ... ) 中的 local 在 bash 中合法(子 shell 继承函数上下文);C++ 臂沿用上一轮 fail-closed(配置了却未就绪即硬失败),PID 由 cr_cleanup 统一回收。

前几轮审查关于三引擎行为对齐、引用计数快照 + CAS acquire 并发语义、stop() bug fix、start() 失败回滚、body 上限、精确路径分发、C++ 单一锁窗口、错误文案字节对齐的结论均不受本次测试增量影响,继续保持。至此这条检查真正守住了 C++ 锁窗口回归。

注:本地未运行测试(命令需审批),结论基于代码走查,最终以 CI 为准。

Fourth-round reflection: a concurrency regression test must prove
overlap (baseline -> background hammer -> poll until the counter
strictly passes the baseline mid-hammer), not assume it — an assertion
that also passes under fully serial scheduling has not pinned the
race. Shared mutable config files cross-pollute per-arm counters; use
per-server copies. index synced.
@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 6a91f1db4f8408d462742bbec835962f9111a043..e01731b765f5049e69c7272f08195e5aa5e824d4
审查截止 e01731b765f5049e69c7272f08195e5aa5e824d4

本轮增量仅一个纯文档 commit(e01731b),无源码变更。

自上次审查截止点 6a91f1d 以来,仅新增 llmdoc/memory/reflections/upstream-serverplus-custom-routes.md 的「第四轮:竞态检查自身的有效性修复」小节,以及 llmdoc/index.md 对应索引条目的同步更新。

  • 复盘内容与上一轮已审查通过的 check [10] 修复(6a91f1d)一致,事实描述准确:记录了「竞态回归测试必须证明重叠而非假设重叠」与「共享可变配置文件是跨臂污染源」两条教训。
  • 无源代码变更,无需检查编译 / 逻辑 / 规范。

前几轮审查关于三引擎行为对齐、引用计数快照 + CAS acquire 并发语义、stop() bug fix、start() 失败回滚、body 上限、精确路径分发、C++ 单一锁窗口、Java skip 计量、错误文案字节对齐的结论均不受本次文档增量影响,继续保持。issue #169 的实现、加固、竞态收尾与复盘至此完整闭环。

代码无问题,已检查增量改动。

@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 e01731b765f5049e69c7272f08195e5aa5e824d4..a4d878d88dc09d3a9f0bd0034180ae8154d18bab
审查截止 a4d878d88dc09d3a9f0bd0034180ae8154d18bab

本轮增量仅一个纯版本号 commit(a4d878d bump: v0.10.13),无逻辑变更。

自上次审查截止点 e01731b 以来,仅有版本号从 0.10.12 升到 0.10.13,涉及 67 个文件、每个 +1/-1。逐项确认全部为版本字符串 bump,无源码逻辑改动:

  • 代码入口一致apple/_version.pypine-go/version.gopine-java/pom.xml 三处版本源同步为 0.10.13
  • fixtures / testdata / pipeline 版本对齐 — 所有 _PINEAPPLE_VERSION 字段同步 bump,跨引擎比对测试的期望版本保持一致,不会引入 cross-validate 版本不匹配。
  • 无遗漏 — 全仓 grep 0.10.12(排除 .git/llmdoc/)无残留,版本升级完整一致。

前几轮审查关于三引擎行为对齐、引用计数快照 + CAS acquire 并发语义、stop() bug fix、start() 失败回滚、body 上限、精确路径分发、C++ 单一锁窗口、Java skip 计量、错误文案字节对齐、竞态检查有效性等结论均不受本次版本 bump 影响,继续保持。issue #169 的实现、加固、竞态收尾、复盘与发版至此完整闭环。

代码无问题,已检查增量改动。

@Liam0205
Liam0205 merged commit 3c0e623 into master Jul 17, 2026
21 checks passed
@Liam0205
Liam0205 deleted the feat/169-runtime-serverplus branch July 17, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: serverplus — reference implementation for embedding pine-go in existing HTTP services

1 participant