perf: prevent import hanging with timeouts and batching
CI / check (push) Successful in 31s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m1s

- verifyAndFixInteractionModesCount: paginated DB queries (500/batch)
  instead of loading all items into memory at once
- withFurniDataLock: add 60s chain timeout to prevent deadlocks
  when a lock holder stalls or crashes
- withGamedataLock: same timeout protection for gamedata locks
- conversion-pool: add 60s per-job timeout, fall back to main thread
- furni/batch: abort signal + post_import progress events + skip
  post-import steps when client disconnects
- furni/batch-regen: abort signal + early exit when disconnected
- clone/sync-all: pre-fetch furnidata once per source instead of
  per item (eliminates 2000+ redundant fetches)
- sse-client: add 60s idle timeout to prevent infinite hangs
This commit is contained in:
openhands committed 2026-08-31 18:25:32 +02:00
1 parent f70d96b81c
commit 7556b1f3fa
5 files changed
+222 -93

No files matched your search

+71 -70
View File
@@ -1128,83 +1128,84 @@ export async function verifyAndFixInteractionModesCount(): Promise<{
let fixed = 0;
let checked = 0;
let skipped = 0;
const BATCH_SIZE = 500;
try {
const allItems = await db
.select({
id: ItemsBase.id,
itemName: ItemsBase.itemName,
publicName: ItemsBase.publicName,
allowSit: ItemsBase.allowSit,
allowLay: ItemsBase.allowLay,
allowWalk: ItemsBase.allowWalk,
interactionType: ItemsBase.interactionType,
interactionModesCount: ItemsBase.interactionModesCount,
})
.from(ItemsBase);
let offset = 0;
while (true) {
const batch = await db
.select({
id: ItemsBase.id,
itemName: ItemsBase.itemName,
publicName: ItemsBase.publicName,
allowSit: ItemsBase.allowSit,
allowLay: ItemsBase.allowLay,
allowWalk: ItemsBase.allowWalk,
interactionType: ItemsBase.interactionType,
interactionModesCount: ItemsBase.interactionModesCount,
})
.from(ItemsBase)
.limit(BATCH_SIZE)
.offset(offset);
for (const item of allItems) {
checked++;
const realFlags = await lookupRealFurniFlags(item.itemName);
if (batch.length === 0) break;
offset += batch.length;
checked += batch.length;
// Real data absent → do not guess, leave the item untouched.
if (realFlags.source === "none") {
skipped++;
continue;
}
for (const item of batch) {
const realFlags = await lookupRealFurniFlags(item.itemName);
const animationModes = await realNitroModes(item.itemName);
const auto = autoDetectInteraction(item.itemName, item.publicName, {
cansiton: realFlags.cansiton,
canlayon: realFlags.canlayon,
canstandon: realFlags.canstandon,
// Furnidata flags are curated → authoritative over keywords.
hasActionData: true,
animationStates: animationModes,
});
if (realFlags.source === "none") {
skipped++;
continue;
}
const changes: Partial<Record<string, unknown>> = {};
// allow_sit/lay/walk of 2 is emulator-special (e.g. tents lay=2,
// walk-through walk=2) — never overwrite those with a 0/1.
if (
item.allowSit !== 2 &&
(item.allowSit ? 1 : 0) !== (auto.canSit ? 1 : 0)
)
changes.allowSit = auto.canSit ? 1 : 0;
if (
item.allowLay !== 2 &&
(item.allowLay ? 1 : 0) !== (auto.canLay ? 1 : 0)
)
changes.allowLay = auto.canLay ? 1 : 0;
if (
item.allowWalk !== 2 &&
(item.allowWalk ? 1 : 0) !== (auto.canStand ? 1 : 0)
)
changes.allowWalk = auto.canStand ? 1 : 0;
if ((item.interactionModesCount ?? 0) !== auto.interactionModesCount)
changes.interactionModesCount = auto.interactionModesCount;
// Only fill interaction_type when the DB still has the generic
// "default" (nothing emulator-specific configured). Never overwrite
// an existing handler type (e.g. intelligence_bookcase, nest, wf_*)
// — those are emulator semantics that real furnidata cannot replace.
if (
(item.interactionType === "default" || !item.interactionType) &&
auto.interactionType !== "default"
) {
changes.interactionType = auto.interactionType;
}
const animationModes = await realNitroModes(item.itemName);
const auto = autoDetectInteraction(item.itemName, item.publicName, {
cansiton: realFlags.cansiton,
canlayon: realFlags.canlayon,
canstandon: realFlags.canstandon,
hasActionData: true,
animationStates: animationModes,
});
if (Object.keys(changes).length > 0) {
try {
await db
.update(ItemsBase)
.set(changes)
.where(eq(ItemsBase.id, item.id));
fixed++;
} catch (err) {
errors.push(
`Failed to fix ${item.itemName}: ${(err as Error).message}`,
);
const changes: Partial<Record<string, unknown>> = {};
if (
item.allowSit !== 2 &&
(item.allowSit ? 1 : 0) !== (auto.canSit ? 1 : 0)
)
changes.allowSit = auto.canSit ? 1 : 0;
if (
item.allowLay !== 2 &&
(item.allowLay ? 1 : 0) !== (auto.canLay ? 1 : 0)
)
changes.allowLay = auto.canLay ? 1 : 0;
if (
item.allowWalk !== 2 &&
(item.allowWalk ? 1 : 0) !== (auto.canStand ? 1 : 0)
)
changes.allowWalk = auto.canStand ? 1 : 0;
if ((item.interactionModesCount ?? 0) !== auto.interactionModesCount)
changes.interactionModesCount = auto.interactionModesCount;
if (
(item.interactionType === "default" || !item.interactionType) &&
auto.interactionType !== "default"
) {
changes.interactionType = auto.interactionType;
}
if (Object.keys(changes).length > 0) {
try {
await db
.update(ItemsBase)
.set(changes)
.where(eq(ItemsBase.id, item.id));
fixed++;
} catch (err) {
errors.push(
`Failed to fix ${item.itemName}: ${(err as Error).message}`,
);
}
}
}
}