Merge pull request 'fix(assets): a busy shard is not a broken one, and a column that reached no existing install (Phase 9a)' (#42) from feat/asset-bridge-p9 into edge
Reviewed-on: #42
This commit is contained in:
@@ -13,6 +13,7 @@ module that follows.
|
|||||||
│ module-uo (>>> HERE <<<) │
|
│ module-uo (>>> HERE <<<) │
|
||||||
│ shard status · spawn atlas · marketplace │
|
│ shard status · spawn atlas · marketplace │
|
||||||
│ governors · cliloc · town crier · uo-link│
|
│ governors · cliloc · town crier · uo-link│
|
||||||
|
│ client files: portraits, item art, names │
|
||||||
└───────────────────────────────────────────┘
|
└───────────────────────────────────────────┘
|
||||||
│ server half: routers, models, schema fragment
|
│ server half: routers, models, schema fragment
|
||||||
│ client half: prebuilt ESM chunk, SPA routes + nav
|
│ client half: prebuilt ESM chunk, SPA routes + nav
|
||||||
@@ -182,7 +183,7 @@ reaches the container.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `UOLINK_BASE_URL` | — | Default sidecar base URL for a site with nothing saved yet. The admin panel's stored value wins. |
|
| `UOLINK_BASE_URL` | — | Default sidecar base URL for a site with nothing saved yet. The admin panel's stored value wins. |
|
||||||
| `UOLINK_WS_URL` | — | Same, for the WebSocket URL. |
|
| `UOLINK_WS_URL` | — | Same, for the WebSocket URL. |
|
||||||
| `UOLINK_PROTOCOL` | `3` | Wire protocol this build speaks. Again only a fallback — set it lower only if you deliberately run an older sidecar. |
|
| `UOLINK_PROTOCOL` | `8` | Wire protocol this build speaks. Again only a fallback — set it lower only if you deliberately run an older sidecar. |
|
||||||
| `TOWNCRIER_DURATION_SEC` | `3600` | How long a published news post's in-game town-crier message stays up (≤ `86400`). |
|
| `TOWNCRIER_DURATION_SEC` | `3600` | How long a published news post's in-game town-crier message stays up (≤ `86400`). |
|
||||||
|
|
||||||
**The sidecar's auth token is deliberately not here.** It is entered in Admin → Shard, encrypted at
|
**The sidecar's auth token is deliberately not here.** It is entered in Admin → Shard, encrypted at
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import api from '../../api.js'
|
import api from '../../api.js'
|
||||||
import { ErrorState, Loading } from '../../core.js'
|
import { ErrorState, Loading } from '../../core.js'
|
||||||
import Row from '../../components/DetailRow.jsx'
|
import Row from '../../components/DetailRow.jsx'
|
||||||
@@ -245,6 +245,28 @@ export default function ClientFiles() {
|
|||||||
load()
|
load()
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
|
// One automatic re-read when the shard answered BUSY (§3.2's single slot),
|
||||||
|
// and exactly one per mount.
|
||||||
|
//
|
||||||
|
// BUSY is not a fault and it is not sticky on the shard — it means something
|
||||||
|
// else held the asset slot for longer than the client's own 425 backoff, and
|
||||||
|
// the two things that hold it are both ordinary: an import the operator
|
||||||
|
// started, and the item-art warm pass refilling itself after a client patch.
|
||||||
|
// The panel does not poll, so without this the operator is left reading a
|
||||||
|
// refusal about a shard that was free again seconds later, until they think to
|
||||||
|
// reload. A second read clears the common case; if it is still busy, the
|
||||||
|
// sentence says to come back, because a page that retried forever would be
|
||||||
|
// holding the slot it is waiting for.
|
||||||
|
const retried = useRef(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (retried.current || busy) return
|
||||||
|
const stillBusy = assets?.code === 'BUSY' || clilocs?.code === 'BUSY'
|
||||||
|
if (!stillBusy) return
|
||||||
|
retried.current = true
|
||||||
|
const t = setTimeout(() => load({ quiet: true }), 4000)
|
||||||
|
return () => clearTimeout(t)
|
||||||
|
}, [assets, clilocs, busy, load])
|
||||||
|
|
||||||
// Every action shares this: run it, say what it said, then re-read status so
|
// Every action shares this: run it, say what it said, then re-read status so
|
||||||
// the panel reflects the world rather than what we assumed happened.
|
// the panel reflects the world rather than what we assumed happened.
|
||||||
async function run(section, table, fn) {
|
async function run(section, table, fn) {
|
||||||
@@ -347,9 +369,22 @@ export default function ClientFiles() {
|
|||||||
style={{ border: '1px solid var(--line)', borderRadius: 10, padding: 16 }}
|
style={{ border: '1px solid var(--line)', borderRadius: 10, padding: 16 }}
|
||||||
className="sans"
|
className="sans"
|
||||||
>
|
>
|
||||||
<strong style={{ color: 'var(--head)' }}>The shard is not answering for client files.</strong>
|
{/* BUSY is the one code here that is not a fault, and saying "the shard
|
||||||
|
is not answering" about it sends an operator to check a shard that is
|
||||||
|
working. The slot is held by something ordinary — an import running,
|
||||||
|
or the warm pass — and it frees itself. */}
|
||||||
|
<strong style={{ color: 'var(--head)' }}>
|
||||||
|
{assets.code === 'BUSY'
|
||||||
|
? 'The shard is busy with another client-file request.'
|
||||||
|
: 'The shard is not answering for client files.'}
|
||||||
|
</strong>
|
||||||
<p style={{ margin: '6px 0 0', color: 'var(--muted)', fontSize: '0.86rem', lineHeight: 1.6 }}>
|
<p style={{ margin: '6px 0 0', color: 'var(--muted)', fontSize: '0.86rem', lineHeight: 1.6 }}>
|
||||||
{assets.reason}
|
{assets.code === 'BUSY'
|
||||||
|
? 'The shard serves one of these at a time, so an import running now — or the' +
|
||||||
|
' item-picture pass refilling itself after a client patch — holds it until it is' +
|
||||||
|
' done. This page re-reads once on its own; if the counts below are still missing' +
|
||||||
|
' after that, reload in a moment.'
|
||||||
|
: assets.reason}
|
||||||
{assets.code === 'DISABLED' &&
|
{assets.code === 'DISABLED' &&
|
||||||
' — set Bridge.AssetsEnabled on the shard to allow it to read its own client files.'}
|
' — set Bridge.AssetsEnabled on the shard to allow it to read its own client files.'}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -957,3 +957,20 @@ UPDATE uo_link_config SET protocol = 8
|
|||||||
WHERE id = 1 AND protocol < 8
|
WHERE id = 1 AND protocol < 8
|
||||||
AND NOT EXISTS (SELECT 1 FROM settings WHERE `key` = 'uo_link_protocol_8_migrated');
|
AND NOT EXISTS (SELECT 1 FROM settings WHERE `key` = 'uo_link_protocol_8_migrated');
|
||||||
INSERT IGNORE INTO settings (`key`, value) VALUES ('uo_link_protocol_8_migrated', '1');
|
INSERT IGNORE INTO settings (`key`, value) VALUES ('uo_link_protocol_8_migrated', '1');
|
||||||
|
|
||||||
|
-- `shard_spawn_points.unique_id` for an install that already had the table
|
||||||
|
-- (Asset Bridge phase 9; the column itself is Events phase 12b).
|
||||||
|
--
|
||||||
|
-- The column was added to the CREATE TABLE above and nowhere else, so it reached
|
||||||
|
-- fresh installs and no existing one -- `CREATE TABLE IF NOT EXISTS` does not add
|
||||||
|
-- a column to a table that is already there, which is what every ALTER in this
|
||||||
|
-- file exists to do. `replaceAtlas` inserts `unique_id` unconditionally, so on an
|
||||||
|
-- upgraded install EVERY spawn-atlas import since v1.2.0 has failed outright with
|
||||||
|
-- `Unknown column 'unique_id' in 'INSERT INTO'` -- the bestiary, the spawn map and
|
||||||
|
-- the champion altars all frozen at whatever was last imported.
|
||||||
|
--
|
||||||
|
-- Found by the phase 9 acceptance walk, on a rig whose tables predate 12b: a fresh
|
||||||
|
-- install cannot reproduce it, and neither can a test whose schema is this file
|
||||||
|
-- applied to an empty database. That is the same blind spot the protocol-pin block
|
||||||
|
-- above records, two phases running.
|
||||||
|
ALTER TABLE shard_spawn_points ADD COLUMN IF NOT EXISTS unique_id VARCHAR(64) NULL;
|
||||||
|
|||||||
Reference in New Issue
Block a user