[2.0 rc.14 / next] SSR: async memo retries run under another concurrent render's context
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức phù hợp với người mới
- 52/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- javascript, typescript
- Lĩnh vực
- frontend
Hướng nghiên cứu
The retry path is subscribePendingRetry → update → run in packages/solid/src/server/signals.ts; compare the captured ctx near lines 1257-1259 with run near lines 1314-1317, which restores the owner but not sharedConfig.context. Reproduce with the failing test in packages/web/test/server/repro-retry-render-context.spec.tsx (run under the server vitest config). Done when that test passes and the existing server suite still passes.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Describe the bug
On the server, an async createMemo that throws NotReadyError is retried when its source settles (subscribePendingRetry → update → run). run restores the memo's owner, but not its render context. The retry, and everything it creates, runs under whichever render last set sharedConfig.context. Under concurrent requests, that is often another request's.
The memo already captures its render's ctx at creation for exactly this reason (packages/solid/src/server/signals.ts:1257-1259: "async re-computations … may run after a concurrent request has overwritten sharedConfig.context"). However, run (signals.ts:1314-1317) never reinstates it.
createSyncMemo has no retry of its own: it re-runs when a reader pulls it, so it inherits its reader's context. That makes it affected whenever its reader is an async memo's retry, and the same fix covers it. The outer memo of children() is one.
An ordinary layout is enough to trigger it: a slot resolved with children(), gated by <Show when={slot() !== undefined}>, whose content waits on data. The Show's when memo is not ready, so it is retried later. The retry resolves the slot, which creates the slot's content under the other request's context.
Impact in a real app. This isn't only a context mismatch: under concurrent requests it can lock up the server. In a Solid 2 SSR app with this layout shape (a page component resolving an optional slot with children(), gated by <Show when={slot() !== undefined}>, whose content waits on data), a cold dev server receiving 18 page loads, 6 at a time, sometimes stops responding. The process sits at 100% CPU and most loads time out (13 of 18 in one run). Pages that do render can fail to hydrate the slot's content (Hydration key miss).
Sampling the stuck process shows the mechanism. The slot content created under the other render gets the same owner id as that render's own memo, so the two share one hydration slot and its deferred. When one memo settles it, the other's reader keeps re-subscribing to the already-settled promise in microtasks, and the event loop never gets a turn.
With the diff below applied to dist/server{,.dev}.js, 10 of 10 runs completed cleanly. Without it, 3 of 10 runs locked up.
Your Example Website or App
Server test on next (a497d4b1), for packages/web/test/server/:
/** @jsxImportSource @solidjs/web */
import { expect, test } from "vitest";
import { renderToStream } from "@solidjs/web";
import { children, createMemo, sharedConfig, Show, type JSX } from "solid-js";
const render = (code: () => any) =>
renderToStream(code).pipeTo(new WritableStream({ write() {} }));
const later = <T,>(value: T, ms: number) => new Promise<T>(r => setTimeout(() => r(value), ms));
// A layout with an optional side slot: resolved once with `children()`, and laid
// out differently when it is present.
function Layout(props: { side?: JSX.Element }) {
const side = children(() => props.side);
return (
<Show when={side() !== undefined} fallback={<main />}>
<aside>{side()}</aside>
<main />
</Show>
);
}
test("a slot resolved by children() is created under its own render's context", async () => {
const contexts: Record<string, unknown> = {};
const seenBy: Record<string, unknown> = {};
// Two requests rendering the same page; each page's slot waits on its own data.
const page = (name: string, ms: number) =>
render(() => {
contexts[name] = sharedConfig.context;
const data = createMemo(() => later(name, ms));
return (
<Layout
side={
<Show when={data()}>
{(() => {
seenBy[name] = sharedConfig.context;
return name;
})()}
</Show>
}
/>
);
});
await Promise.all([page("A", 5), page("B", 10)]);
expect(seenBy.A).toBe(contexts.A);
expect(seenBy.B).toBe(contexts.B);
});
Steps to Reproduce the Bug or Issue
In packages/web, run pnpm exec vitest run --config vite.config.server.mjs test/server/repro-retry-render-context.spec.tsx.
Expected behavior
Each page's slot content is created under its own render's context. Instead, page A's is created under page B's, and the test fails.
Restoring the captured context in run makes the test pass, and the existing server suite still passes:
const run = () => {
resetOwnerForRerun(owner);
- return runWithOwner(owner, () => runWithObserver(comp, () => comp.compute(comp.value)));
+ const prevCtx = sharedConfig.context;
+ sharedConfig.context = ctx;
+ try {
+ return runWithOwner(owner, () => runWithObserver(comp, () => comp.compute(comp.value)));
+ } finally {
+ sharedConfig.context = prevCtx;
+ }
};
Platform
- solid-js / @solidjs/web:
next@ a497d4b1, and 2.0.0-rc.14 - Node 24.20, macOS
- Ngôn ngữ chính
- TypeScript
- Star
- 36.1k
- Fork
- 1.1k
- Merge trung bình
- 9 giờ 56 phút
- Pull request đã merge (30 ngày)
- 336
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của solidjs/solid
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 66/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 12/100
solidjs/solid#3975 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 Nửa ngày Mức phù hợp với người mới 32/100
solidjs/solid#3974 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
hydrate() run mid-parse leaves dead server nodes beside fresh client copies (async entry script)Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 20/100
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của solidjs/solid
Issue tương tự
-
cleanName keeps a state code that's joined to the word before it (SavannahGa → "Savannah Ga")Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
openwatersio/slackwater-database#281 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
accepted bug via-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
pingdotgg/t3code#18690 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Bayern feature:Application ready web
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
digitalfabrik/entitlementcard#3176 ·
Maintainer thường phản hồi trong vòng 7 ngày
-
[BUG] 运行位置的分支列表不自动刷新,外部新建的分支看不到Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
NanmiCoder/cc-haha#1533 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Maintainer thường phản hồi trong vòng 1 ngày