Headless-tila (palvelin)

Muista

Tässä osiossa kuvataan blunderDB:n edistynyt ja valinnainen tila, joka on tarkoitettu palvelinkäyttöön, monikäyttäjäympäristöihin ja automaatioon. blunderDB:n tavanomainen ja suositeltu käyttötapa on edelleen työpöytäsovellus, joka kuvataan edellisissä luvuissa. Jos käytät blunderDB:tä yksin omalla tietokoneellasi, et tarvitse tätä tilaa: voit ohittaa tämän luvun menettämättä mitään analyysiominaisuuksista.

Yleiskatsaus

Sama blunderdb-binääri voi työpöytäsovelluksen ja komentorivikomentojen (katso Komentoriviliittymä (CLI)) lisäksi toimia headless-tilassa: ilman graafista käyttöliittymää, ohjattuna kokonaan komentoriviltä tai verkon kautta. Tämä tila kattaa kolme käyttötapaa:

  • demoni serve — tarjoaa blunderDB:n moottorin HTTP + JSON -palveluna, jotta jaettua tietokantaa voidaan pyörittää palvelimella ja käyttää usean käyttäjän kesken;

  • yleiskäyttöinen call-välittäjä — kutsuu mitä tahansa tallennustoimintoa suoraan paikallisesti, skriptausta ja testausta varten;

  • migrate-komento — siirtää yhden käyttäjän SQLite-tietokannan monikäyttäjäiseen PostgreSQL-taustajärjestelmään.

Nämä kolme käyttötapaa nojaavat yhteiseen tallennuskerrokseen, joka osaa puhua kahdelle taustajärjestelmälle: SQLite (työpöytäsovelluksen tavanomainen .db-tiedostomuoto) ja PostgreSQL (monikäyttäjäisiin palvelinkäyttöönottoihin).

serve-demoni

blunderdb serve käynnistää moottorin HTTP-palveluna, joka vastaa JSON-muodossa. Sen avulla voi isännöidä asematietokantaa yhdellä koneella ja käyttää sitä useasta asiakkaasta.

# sqlite
blunderdb serve --db database.db --addr 127.0.0.1:8080

# postgres
blunderdb serve --backend postgres \
    --dsn "postgres://user:pass@host:5432/blunderdb?sslmode=disable" \
    --addr 127.0.0.1:8080

Muista

sslmode=disable sopii vain luotettuun yksityisverkkoon — tietokanta viereisessä kontissa, verkossa, josta ei ole reittiä isäntäkoneeseen eikä internetiin. Etätietokannalle sslmode=require salaa yhteyden ja verify-full tarkistaa lisäksi palvelimen varmenteen ja sen isäntänimen. Tämän sivun muissa yhteysmerkkijonoissa on sslmode=disable samasta syystä: ne kaikki kuvaavat yksityisverkkoa.

Varoitus

Demoni ei suorita minkäänlaista todennusta. Se luottaa X-Tenant-ID-pyyntöotsakkeeseen ja on ajettava todennuksesta huolehtivan käänteisvälityspalvelimen (nginx, Caddy…) takana. Älä koskaan altista sitä suoraan julkiseen internetiin.

X-Tenant-ID on tenantin kokonaisluku (1, 2, 42…): käänteisvälityspalvelimen tehtävä on yhdistää todennettu tili tähän kokonaislukuun. Nimi (alice) hylätään vastauksella 400 invalid, sitä ei koskaan muunneta.

Valinnat:

Valinta

Oletus

Merkitys

--db <polku>

–

SQLite-tiedosto (oikotie komennolle --backend sqlite --dsn <polku>)

--backend <tyyppi>

sqlite

tallennustaustajärjestelmä: sqlite tai postgres

--dsn <merkkijono>

$BLUNDERDB_DSN

taustajärjestelmän yhteysmerkkijono

--addr <isäntä:portti>

:8080

kuunteluosoite

--log-level <taso>

info

lokitustaso: debug|info|warn|error

--metrics

true

tarjoaa /metrics (Prometheus-muoto)

--web

false

tarjoilee selauskäyttöisen verkkosivun osoitteessa /app/; oletuksena pois päältä, ks. alempaa

--direction

false

palvelee turnauksen ja tapahtuman johtotoiminnot; oletuksena pois päältä, ks. Johtotoiminnot

--mcp-write

false

tarjoaa /mcp:n kirjoitustyökalut; oletuksena pois, ks. Työkalut tekoälyavustajalle (MCP)

--transcription

false

palvelee litterointieleet (transcriptions.create, apply, finish…); oletuksena pois päältä, ks. Litterointi rajapinnan kautta

--transcription-ttl <kesto>

30m

sulkee litterointi-istunnon, joka on ollut käyttämättä tätä kauemmin

--cors-allow-origin <alkuperä>

–

ottaa CORSin käyttöön tälle alkuperälle, pilkuin erotellulle alkuperäluettelolle tai arvolle * (oletuksena pois käytöstä); vastaus heijastaa pyynnön alkuperän vain, jos se on luettelossa, ja sisältää otsakkeen Vary: Origin

--rate-limit-rps <n>

50

pyyntöraja sekunnissa tenanttia kohti (0 = pois käytöstä); käytössä oletuksena anteliaalla arvolla valinnaisen sijaan, jotta pelkkään tietokantaan keskittyvä compose-tiedosto ei perisi täysin rajoittamatonta demonia

--rate-limit-burst <n>

100

token-ämpärin koko pyyntöpiikkejä varten

--quota-positions <n>

0

positioiden määrä, jonka tenant voi tallentaa, tarkistetaan tuonnin alussa: kun raja on saavutettu, tuonti hylätään (413, storage_quota_exceeded); positions.save ja muut yksittäiset kirjoitukset eivät ole rajoitettuja; 0 = rajoittamaton

--quota-analysis-seconds <n>

0

moottorin laskenta-aika CPU-sekunteina tenanttia ja UTC-vuorokautta kohden (429, quota_exceeded); 0 = rajoittamaton

--quota-imports <n>

0

saman tenantin samanaikaisesti käynnissä olevat tuonnit (429, quota_exceeded); 0 = rajoittamaton

--rls

false

PostgreSQL: ottaa käyttöön tenant-kohtaisen Row-Level Securityn (syvyyspuolustus, valinnainen)

--read-tenants

false

noudattaa X-Read-Tenants-otsakkeita across.*-lukuissa; poissa käytöstä otsake hylätään (400) — katso Useiden tenanttien lukeminen

--bearoff-ts <tiedosto>

–

valinnainen kaksipuolinen bearoff-tietokanta (.bd), joka laajentaa TS-06-06-taulukkoa EPC-päätepisteen kilpa-analyysia varten; taustapalvelu ei koskaan lataa tietokantaa — katso Bearoff-tietokannat

--identity-dir <hakemisto>

–

demonin liikkeeseenlaskijaidentiteetin hakemisto (luodaan ensimmäisellä käyttökerralla); tarvitaan, jotta exports.sqlite voi kirjoittaa vesileiman — katso alempana

--ops-addr <isäntä:portti>

–

tarjoaa /ops/-perheen (maintenance.vacuum, tenant.purge) osoitteessa, joka on erillinen --addr-osoitteesta, ja poistaa sen siltä; tyhjä (oletus) jättää ne pääkuuntelijaan, jossa etuliitteen torjuminen on välityspalvelimen tehtävä — ks. Ylläpitoreitit

--pprof-addr <isäntä:portti>

–

julkaisee net/http/pprof-rajapinnan osoitteessa, joka on erillinen --addr-osoitteesta (oletuksena pois päältä); vain virheenjäljitykseen — nämä päätepisteet eivät tunne tenantteja ja antavat koko prosessin muisti- tai suoritinprofiilin, eikä niitä saa koskaan julkaista julkisesti eikä samassa osoitteessa kuin /v1

Useimmat asetukset voi antaa myös ympäristömuuttujalla (BLUNDERDB_BACKEND, BLUNDERDB_DSN, BLUNDERDB_ADDR, BLUNDERDB_LOG_LEVEL, BLUNDERDB_METRICS, BLUNDERDB_CORS_ALLOW_ORIGIN, BLUNDERDB_RATE_LIMIT_RPS, BLUNDERDB_RATE_LIMIT_BURST, BLUNDERDB_RLS, BLUNDERDB_READ_TENANTS, BLUNDERDB_TS_PATH, BLUNDERDB_IDENTITY_DIR, BLUNDERDB_OPS_ADDR, BLUNDERDB_PPROF_ADDR): nimenomainen lippu on ensisijainen vastaavaan muuttujaan nähden.

Demonilla ei ole datakansion valitsinta: se kirjoittaa bearoff-taulukkonsa hakemistoon $XDG_DATA_HOME/blunderdb, tai sen puuttuessa ~/.local/share/blunderdb. Niitä siirretään siis muuttujalla XDG_DATA_HOME — katso Bearoff-tietokannat.

Nopeusrajoittimen ämpäritaulukolla on itsellään kova yläraja (10 000 erillistä tenanttia): sen ylittyessä jokainen uusi tenant häätää vähiten äskettäin käytetyn ämpärin sen sijaan, että taulukko kasvaisi rajattomasti — hyödyllistä, jos asiakas lähettää monia erillisiä X-Tenant-ID-arvoja, tahallisesti tai ei, kahden määräaikaisen käyttämättömien ämpäreiden siivouksen välillä.

blunderdb serve hylkää nyt jokaisen odottamattoman positionaalisen argumentin (lukuun ottamatta ainoaa alkuun jäävää serve-sanaa, jonka jo pelkkään binaariin supistettu ENTRYPOINT päästää läpi): ilman tätä tarkistusta tällaisen argumentin jälkeen sijoitettu lippu jätettiin hiljaisesti huomiotta — docker run image serve --addr :9090, luonnollinen refleksi, koska imagen ENTRYPOINT on jo serve, käynnistyi porttiin :8080 sanaakaan sanomatta.

Päätepisteet

Palvelu tarjoaa hallinnolliset päätepisteet, jotka ovat aina läsnä:

  • GET /healthz — elossaolo (prosessi on käynnissä);

  • GET /readyz — valmius (tallennus vastaa ja sen skeema on odotetussa versiossa);

  • GET /metrics — Prometheus-metriikat (jos --metrics on käytössä);

  • GET /app/ — selauskäyttöinen verkkosivu (jos --web on käytössä).

Verkkosivu

blunderdb serve --web tarjoilee sivun osoitteessa /app/: kirjaston, jota voi selata tabletilta tai puhelimelta ilman asennuksia.

Se osaa kolme asiaa, ja tuo lista on päätös, ei vaihe:

  • selata asemaa, sen analyysiä ja lautaa;

  • hakea samalla tunnuskieliopilla kuin sovelluksen komentorivi;

  • kerrata Anki-pakkaa — vastaus paljastetaan ja arvosana annetaan.

Se ei osaa muokata asemaa, tuoda, poistaa eikä hallita kokoelmia, otteluita, turnauksia tai asetuksia, eikä opi. Täältä puuttuva toiminto ei ole aukko: se on rajaus.

Se on oletuksena pois päältä, ja tuo oletus on päätös. Palvelin ei tunnista ketään: se luottaa X-Tenant-ID-otsakkeeseen ja sen on ajettava tunnistavan välityspalvelimen takana. Selaimella tavoitettavan käyttöliittymän toimittaminen valmiiksi päällä kutsuisi juuri sen käyttöönoton, jonka tuo sääntö kieltää.

Sivu ei lähetä tenanttia: välityspalvelin asettaa otsakkeen, kuten kaikille muillekin asiakkaille. Paikallisessa kehityksessä, ja vain siellä, /app/?tenant=1 nimeää sellaisen — mikä ei muuta mitään sellaisen palvelimen turvallisuudessa, joka jo hyväksyy tuon otsakkeen keneltä tahansa.

Sivun tiedostot tarjoillaan ilman tenanttia, tarkoituksella: selaimen on voitava ladata sivu ennen kuin välityspalvelin osoittaa sille mitään, eikä sivu sisällä dataa.

Elossaolo ja valmius vastaavat kahteen eri kysymykseen. /healthz vastaa aina 200 heti kun prosessi palvelee pyyntöjä, kysymättä koskaan tallennukselta: orkestroija käynnistää uudelleen kontin, jonka elossaolotarkistus epäonnistuu, eikä hetkellisesti tavoittamaton tietokanta saa käynnistää tervettä taustaprosessia uudelleen silmukassa. /readyz vastaa 503 (status-arvolla down tai version_mismatch) niin kauan kuin tietokanta ei vastaa tai sen skeema ei ole binäärin skeema: liikenne vain ohjataan muualle, kunnes se palaa.

Alikomento blunderdb healthcheck (mukana myös konttikuvan serve-binäärissä) tekee GET /readyz-pyynnön paikalliselle taustaprosessille ja palauttaa 0, jos se on valmis, muuten 1; osoite on --addr-valitsimen tai BLUNDERDB_ADDR-muuttujan osoite, oletuksena :8080. Se on Docker-kuvan HEALTHCHECK ja sopii yhtä hyvin skriptiin tai systemd-yksikköön:

blunderdb healthcheck --addr 127.0.0.1:8080 && echo ready

Liiketoimintapinta noudattaa muotoa POST /v1/<perhe>.<metodi> (esimerkiksi /v1/positions.save, /v1/matches.get). Perheet kattavat asemat, analyysit, ottelut, kommentit, kokoelmat, turnaukset, Anki-kortit, suodattimet, istunnot, historian (haku ja komennot), haun, metatiedot, kirjaston asetukset, tilastot sekä tuonnin ja viennin. Listauspäätepisteet palauttavat NDJSON-virran (yksi JSON-objekti riviä kohden). Palvelin sammuu siististi signaalilla SIGINT / SIGTERM.

Virhe palauttaa kuoren {"error":{"code":…,"message":…}}. Koodi not_found kertoo, ettei nimettyä resurssia ole; unknown_route, sekin 404, kertoo, ettei palvelu tarjoa kutsuttua metodia: asiakas ja palvelu ovat eri versioita, tai palvelu tarjoaa perheen vain valitsimen kanssa. Asiakas päättelee tiedon puuttuvan vain koodista not_found.

positions.save palauttaa {"id":…,"created":…}. created on true vain sille kutsulle, joka lisäsi aseman, ja sen kertoo kirjoitus itse: asiakas, joka kopioi aseman ja sitten sen analyysin ja jonka on peruttava kopio virheen jälkeen, poistaa aseman vain, jos se loi sen, ilman edeltävän positions.exists-kutsun kilpatilannetta.

Mitä /v1 lupaa

Asiakasohjelman, joka on kirjoitettu /v1:tä vasten, on jatkettava toimintaansa. Sääntö mahtuu kolmelle riville, ja se on kirjoitettuna hyödyllisempi kuin arvattuna:

  • Se mikä on olemassa ei muuta merkitystään. /v1-reittiä ei nimetä uudelleen, ei poisteta eikä anneta uutta merkitystä. Pyyntö- tai vastauskenttää ei nimetä uudelleen, ei poisteta eikä muuteta tyypiltään.

  • Se mikä lisätään, lisätään. Uusi reitti, valinnainen pyyntökenttä, uusi kenttä vastauksessa: asiakas joka ne ohittaa jatkaa toimintaansa — se on tässä valittu ”yhteensopivan” määritelmä. Asiakkaan on siis ohitettava tuntemattomat kentät eikä hylättävä niitä.

  • Loput on /v2. Aiemmin vapaaehtoisen kentän tekeminen pakolliseksi, yksikön vaihtaminen, virhekoodin merkityksen muuttaminen: nämä ovat rikkoutumisia, ja ne asuvat toisen etuliitteen alla, /v1:n rinnalla, niin kauan kuin asiakkaat siirtyvät.

Kaksi tarkennusta joilla on merkitystä. /ops/-reitit eivät kuulu piiriin: ne palvelevat käyttöönoton ylläpitoa, muuttuvat sen mukana, eivätkä ole API kolmansien ohjelmille. Ja sopimus itse tuotetaan demonin reittitaulusta (openapi.yaml, API-sopimus): se ei voi kuvata muuta kuin sitä mitä palvelin tarjoaa.

Litterointi rajapinnan kautta

Perhe transcriptions.* antaa ulkoisen asiakkaan litteroida ottelun ele eleeltä samalla logiikalla kuin työpöytäsovellus. Lukemiset (list, get, exportMat, losses) palvellaan aina. Eleet (create, open, editMatch, apply, undo, redo, close, finish, abandon) palvellaan vain valinnalla serve --transcription: ilman tätä lippua nämä reitit vastaavat 404.

create ja open palauttavat luonnoksen tilan, sen revision-arvon ja sessionId-tunnisteen. apply, undo, redo, close ja finish nimeävät tämän sessionId-tunnisteen: puuttuu → 400, vanhentunut tai tuntematon istunto → 410; asiakas avaa silloin luonnoksen uudelleen (open), kohdistin asiakirjan lopussa. abandon ei nimeä istuntoa: se poistaa luonnoksen pelkän If-Match-revision perusteella. Jokainen kirjoittava ele kantaa viimeksi nähdyn revision If-Match-otsakkeessa ja palauttaa seuraavan:

  • puuttuva If-Match → 428;

  • vanhentunut revisio → 409; virhekuori antaa nykyisen revision (details.revision) ja luonnoksen tuoreen tilan (details.state: asiakirja, revisio, istunto ja kohdistin), jonka asiakas näyttää ennen eleensä toistamista, jos se on vielä voimassa.

Revisio etenee vain, kun asiakirja muuttuu (otsikko ja toiminnot): kohdistimen siirtäminen tai käynnissä olevan toiminnon nopan syöttäminen ei kirjoita mitään ja palauttaa saman revision. Istunto kuuluu luonnokselle, ei asiakkaalle: open palauttaa elävän istunnon, jos sellainen on, ja sen jakavat välilehdet tai työasemat jakavat myös kohdistimen ja kumoamispinon.

Istunto säilyttää vain kumoamispinon, kohdistimen ja keskeneräisen syötön: luonnos kirjoitetaan jokaisen sen muuttavan eleen jälkeen, joten kadonnut istunto (toimettomuus, uudelleenkäynnistys, toinen instanssi) ei hävitä yhtään elettä. transcriptions.get palauttaa revision ETag-otsakkeena ja vastaa 304 If-None-Match-otsakkeeseen, joka nimeää sen.

finish tallentaa ottelun ja poistaa luonnoksen, abandon poistaa sen ilman ottelua, close vapauttaa vain istunnon. editMatch avaa luonnoksen olemassa olevasta ottelusta ja palauttaa tuodun ottelun osalta niiden analyysien ja kommenttien määrän, joita litterointi ei säilytä (losses.lossy). Tallennetun ottelun analyysi käynnistetään komennolla gammonnet.analyzeMissing.

Varoitus

Palvelin ei tunnista ketään: kirjoituksen avaaminen tarkoittaa sen uskomista välityspalvelimelle (Käyttöönotto todentavan proxyn takana). Rooli ”litteroija” on välityspalvelimen sääntö etuliitteelle /v1/transcriptions., ei palvelimen käsite.

Python-asiakas

clients/python/ sisältää minimaalisen asiakkaan ilman riippuvuuksia vakiokirjaston ulkopuolelle — demoni puhuu POSTia ja JSONia, jotka urllib ja json kattavat kokonaan:

from blunderdb import Client

api = Client("http://127.0.0.1:8080", tenant=1)
print(api.metadata_counts())

for position in api.positions_list({"limit": 10}):
    print(position["id"])

Se on kahtena puoliskona, ja se on tarkoituksellista. _generated.py kantaa yhden metodin reittiä kohden, tuotettuna demonin reittitaulusta komennolla go run ./cmd/openapi-gen: käsin kirjoitettu pinta ajautuisi erilleen sinä päivänä kun reitti lisätään, eikä kukaan huomaisi ennen kuin käyttäjä huomaa. client.py kantaa siirtokerroksen — istunnon, tenant-otsakkeen, virhekuoren, NDJSON:n luvun — ja on kirjoitettu käsin. Se mikä muuttuu API:n mukana tuotetaan; se mikä muuttuu harkinnan mukana ei.

Metodien nimet ovat perhe_operaatio snake_case-muodossa: /v1/positions.loadByIds on positions_load_by_ids(). Perhe säilytetään, koska useat perheet jakavat operaation nimen (list, delete), ja pelkkä list() törmäisi.

events() seuraa reittiä /v1/events ja palauttaa yhden sanakirjan viestiä kohden (ks. Toiminnoista ilmoittaminen: /v1/events).

Epäonnistuminen nostaa APIError-poikkeuksen, joka kantaa demonin kuoren sellaisenaan: code (mihin ohjelma haarautuu), message (mitä ihminen lukee), HTTP-status ja yksityiskohdat.

Moottorin upottaminen Go-ohjelmaan

pkg/blunderdb/server.Bootstrap avaa tallennuksen ja palauttaa joukon käsittelijöitä kutsuvassa prosessissa, kuuntelematta porttia. Se on sisäänkäynti luotetulle vanhemmalle — gammonGo:lle — joka haluaa asemakirjaston ajamatta demonia vierellä tai puhumatta HTTP:tä itselleen.

Se mitä tämä olettaa sanotaan suoraan: vanhempi on luotettu. Ei ole tenanttia tarkistettavaksi, ei otsaketta vahvistettavaksi, ei nopeusrajoitinta — ne kuuluvat demonille koska se kohtaa verkon, ja ADR-0005 kertoo miksi. Moottorin upottava ohjelma valitsee oman tenanttinsa ja vastaa kutsuistaan.

Turnauksen johto ja tapahtumat

Työasemalla johdetut turnaukset ja ne ryhmittelevät tapahtumat (rencontre API:ssa ja sen reiteissä /v1/rencontres.*) luetaan API:n kautta kutsujan tenantin alla, samalla koodilla kuin työasemalla. Luku palvellaan aina; toiminnot (tuloksen syöttö, parien muodostus, tapahtuman luonti) palvellaan vain valinnalla serve --direction (Johtotoiminnot).

  • directions.list ja directions.directory lukevat koko tenantin: johdettujen turnausten luettelon ja pelaajahakemiston.

  • Muut directions.*-reitit ottavat parametrin {"tournamentId": N}: directions.get (täysi näkymä: ehdotukset, sijoitukset, käynnissä olevat ottelut), directions.participants, directions.freeParticipants, directions.tableGrid, directions.brackets, directions.standings, directions.standingsCsv, directions.history (valinnaiset suodattimet player ja match), directions.clock, directions.slots, directions.lastDecision, directions.pageHtml ja directions.pairingSheetHtml (parametrilla round).

  • rencontres.list, sitten rencontres.get ja rencontres.pageHtml parametrilla {"id": N}. rencontres.pageHtml tuottaa huoneen seinänäyttösivun, itsenäisen HTML-dokumentin kentässä html: seinänäyttö näyttää sen ja lukee sen säännöllisesti uudelleen.

  • rencontres.ranking palauttaa kauden sijoituksen, kuten blunderdb tournament ranking --season: rencontreId, from, to, points, participation ja elo, kaikki valinnaisia; ilman rencontreId:tä tai ajanjaksoa kaikki vuokralaisen (tenant) johdetut turnaukset lasketaan mukaan.

Sivut tuotetaan ranskaksi, johtomoottorin kielellä. Turnaus, jota ei johdeta tai joka kuuluu toiselle tenantille, vastaa 404.

Ehdolliset lukemiset. Jokainen näistä reiteistä palauttaa ETag-otsakkeen. Kun se lähetetään takaisin If-None-Match-otsakkeessa, saadaan 304 ilman runkoa niin kauan kuin mikään reitin lukema ei ole muuttunut. Jokainen kirjoitus muuttaa ETag-arvon heti: toimi turnauksessa tai saman tapahtuman turnauksessa, ottelun liittäminen, paikasta aloitettu luonnos, turnauksen uudelleennimeäminen, tapahtuman muutos. Vastaus 304 ei toista yhtään turnausta, mikä tekee muutaman sekunnin välein kyselevästä seinänäyttösivusta edullisen. Vain ajasta riippuva on poikkeus: ehdotukset, kello ja sivut lasketaan lukuhetkellä, joten ETag on voimassa enintään minuutin. Uudelleen lukeva asiakas näkee näin määräajan tai tauon kuluvan minuutin sisällä.

Nämä reitit ovat POST-pyyntöjä. Tälle verbille RFC 9110 (§13.1.2) vastaa 412 täsmäävään If-None-Match-otsakkeeseen. Daemon vastaa kuitenkin 304: pyynnön runko sisältää vain vaikutuksettoman lukemisen parametrit, ja se käyttäytyy kuin GET. Muoto If-None-Match: * hylätään (400), koska se ei osoita mitään vastausta, joka asiakkaalla jo olisi. Virheellinen pyyntö (esimerkiksi negatiivinen round) hylätään ennen mitään ehtoa.

curl -si -X POST http://127.0.0.1:8080/v1/rencontres.pageHtml \
  -H 'X-Tenant-ID: 1' -d '{"id":1}' | grep -i '^etag'
curl -si -X POST http://127.0.0.1:8080/v1/rencontres.pageHtml \
  -H 'X-Tenant-ID: 1' -H 'If-None-Match: W/"…"' -d '{"id":1}'
# HTTP/1.1 304 Not Modified

Kuten muutkin /v1-reitit, nämä reitit eivät tunnista ketään: välityspalvelimen (proxy) takana (Käyttöönotto todentavan proxyn takana) jokainen, joka yltää tenantin /v1/directions.-etuliitteeseen, lukee sen turnaukset pelaajien nimet mukaan lukien. Proxy, joka varaa nämä lukemiset tietyille käyttäjille, tekee sen säännöllä tälle etuliitteelle ja etuliitteelle /v1/rencontres..

Johtotoiminnot

blunderdb serve --direction avaa toiminnot, joita työasema tekee johdetulle turnaukselle ja tapahtumalle. Ilman tätä lippua nämä reitit vastaavat 404, kuin niitä ei olisi. call palvelee ne aina.

  • directions.create (tournamentId, config, seed), directions.setConfig ja directions.previewConfig (config, moottorin JSON-muotoinen asetus);

  • ilmoittautumiset: directions.enterParticipants (players), directions.addParticipant (name, club, rating; arvoilla section ja key myöhästyjä saa vapaakierrospaikan), directions.updateParticipant, directions.withdraw, directions.reinstate, directions.makeAbsent, directions.makeAvailable, directions.addPair, directions.updatePair;

  • kulku: directions.confirmProposal (action, sellaisena kuin directions.get sen ehdottaa), directions.confirmAllProposals, directions.startMatch, directions.enterResult, directions.enterForfeit, directions.moveMatchToTable, directions.cancelMatch, directions.correctResult, directions.close, directions.reopen, directions.addNote, directions.attachMatch, directions.detachMatch;

  • tapahtuma: rencontres.create, rencontres.update, rencontres.attach, rencontres.detach, rencontres.trash, rencontres.setTableOutOfService, rencontres.setBreaks;

  • pöytien ominaisuudet: rencontres.setTables (id, tableSettings, yksi merkintä jokaista pöytää kohti, jolla on niitä: numero, nimi, sali, varattu, osoitettu), rencontres.setEventRooms (id, tournamentId, rooms, salit, joissa kilpailu pelataan; ei yhtään tarkoittaa kaikkia pöytiä) ja directions.setTables (tournamentId, tableSettings) yksin pelattavalle kilpailulle.

Turnaustoiminto palauttaa turnauksen täydellisen näkymän, kuten directions.get; tapahtumatoiminto palauttaa tapahtuman. Palvelu kirjoittaa sen jälkeen näyttösivut uudelleen kansioon, jonka tietokanta osoittaa, kuten työasemalla. Sivu, jota ei voi kirjoittaa (kansio kadonnut, levy täynnä), ei peruuta toimintoa: vastaus sisältää Direction-Page-Warning-otsakkeen jokaisesta kirjoittamatta jääneestä sivusta (tournament 3, rencontre 2) ilman palvelimen polkua, ja työasema näyttää sen tilarivillään.

Toiminto, jonka säännöt hylkäävät (tyhjä nimi, varattu pöytä, turnaus, joka ei ole alkanut, moottorin hylkäämä kokoonpano), palauttaa 400 ja syyn. Daemonin tai sen tietokannan vika palauttaa 500 ilman yksityiskohtia: syy jää daemonin lokiin.

Versio on pakollinen. Jokainen turnauksen tai tapahtuman luku palauttaa otsakkeen Direction-Version, ja jokainen toiminto lähettää sen takaisin otsakkeessa If-Match:

  • ilman If-Match-otsaketta (tai arvolla *) toiminto hylätään: 428;

  • jos joku on kirjoittanut tämän luvun jälkeen, toiminto hylätään: 409. Virheen details-kenttä sisältää tuoreen tilan ja sen version: asiakas lukee uudelleen ja toistaa toimintonsa, jos se on edelleen kelvollinen;

  • muuten toiminto toteutetaan ja se palauttaa uuden version otsakkeessa Direction-Version.

Vertailu tehdään toiminnon transaktiossa tietokannan lukon alla (PostgreSQLin neuvoa-antava lukko turnausta tai tapahtumaa kohti, SQLiten kirjoituslukko): kahdesta samalle lukemalle lähetetystä toiminnosta vain yksi toteutuu, kulkivatpa ne saman daemonin, kahden samaa PostgreSQL-tietokantaa käyttävän daemonin tai työaseman ja call-komennon kautta samaan tiedostoon. Toiminto kirjoitetaan kokonaan tai ei lainkaan. Tapahtumassa pelatulla turnauksella on sen tapahtuman versio, joten toiminto sisarkilpailussa muuttaa myös sen. directions.create ja rencontres.create eivät kohdista mihinkään olemassa olevaan eivätkä ota versiota.

Idempotenssi. Toiminto, jolla on Idempotency-Key-otsake, toteutuu vain kerran: samalla avaimella uudelleen lähetettynä se palauttaa ensimmäisen vastauksen otsakkeineen (Direction-Version mukaan lukien) ja Idempotency-Replayed: true. Kaksoisnapsautus tai verkon uudelleenyritys ei kirjaa kahta tulosta; kaksi samanaikaista saman avaimen lähetystä suorittaa toiminnon vain kerran. Vain onnistunut vastaus säilytetään.

  • Avain on sidottu pyynnön runkoon: sama avain eri rungolla palauttaa 422.

  • Uudelleentoisto tulee ennen versiotarkistusta: se palauttaa säilytetyn vastauksen ilman 428 tai 409, vaikka versio olisi sittemmin muuttunut.

  • Avaimet elävät muistissa, kussakin daemonin instanssissa, 24 tuntia, enintään 1 000 per tenant: uudelleenkäynnistys unohtaa ne, eikä toinen instanssi tunne niitä.

curl -si -X POST http://127.0.0.1:8080/v1/directions.get \
  -H 'X-Tenant-ID: 1' -d '{"tournamentId":3}' | grep -i '^direction-version'
curl -s -X POST http://127.0.0.1:8080/v1/directions.enterResult \
  -H 'X-Tenant-ID: 1' -H 'If-Match: "…"' -H 'Idempotency-Key: t4-r2' \
  -d '{"tournamentId":3,"matchId":"m7","winner":"aa","scoreA":7,"scoreB":3}'

Varoitus

Palvelin ei tunnista ketään (ADR-0005). Valinnalla --direction kuka tahansa, jonka välityspalvelin päästää läpi, syöttää tuloksia. Moottori ei tunne rooleja (johtaja, tuomari, lukija): rooli on välityspalvelimen sääntö, joka varaa /v1/directions. ja /v1/rencontres. johtajille tai päästää läpi vain lukemiset. Älä koskaan käynnistä --direction tavoitettavissa olevalla palvelimella ilman tällaista välityspalvelinta, ei edes seuran Wi-Fi-verkossa.

Toiminnoista ilmoittaminen: /v1/events

GET /v1/events on Server-Sent Events -virta (text/event-stream): yksi viesti tenantin jokaista hyväksyttyä toimintoa kohden, julkaistu tietokantaan kirjoittamisen jälkeen, ei koskaan hylätystä tai peruutetusta toiminnosta. Viesti kertoo, mikä on muuttunut, ja sen uuden version, ei tilaa: asiakas lukee näyttämänsä uudelleen If-None-Match-otsakkeella.

  • event: rencontre — rencontreId, tournamentIds (tapahtuman kilpailut ennen toimintoa ja sen jälkeen) ja version;

  • event: direction — tournamentId ja version, tapahtuman ulkopuolella pelattavalle turnaukselle;

  • event: transcription — transcriptionId ja revision; hylätyllä tai päätetyllä luonnoksella on removed (ja matchId Päätä-toiminnolla).

removed: true ilmaisee, mitä ei enää ole. Reitti palvellaan vain valinnalla --direction tai --transcription: ilman niitä daemon ei kirjoita mitään ilmoitettavaa, ja /v1/events vastaa 404. Kuten jokainen /v1/-reitti, se vaatii X-Tenant-ID:n: tilaaja kuulee vain oman tenantinsa. Tenant pitää auki enintään 16 virtaa kerrallaan; sen yli 429. Työasema käyttää samaa palvelua mutta ei kytke siihen väylää: sen toimintoja ei ilmoiteta.

Parametrit tournament, rencontre ja transcription (pilkuilla erotetut tai toistetut tunnisteet) rajaavat tilausta: viesti läpäisee, jos se nimeää jonkin niistä. Tapahtuman turnaus saa tapahtumansa viestit. Tuntematon parametri tai virheellinen tunniste palauttaa 400.

curl -N http://127.0.0.1:8080/v1/events?rencontre=2 -H 'X-Tenant-ID: 1'

Ei historiaa. Daemon ei säilytä yhtään viestiä. Jokainen virta alkaa viestillä event: resync ja id:llä: asiakas on voinut missata toimintoja ennen yhdistämistä tai kahden yhteyden välillä, ja se lukee kaiken näyttämänsä uudelleen. Syy on reconnected, kun pyyntö sisältää Last-Event-ID:n, muuten subscribed. Liian hidas tilaaja, jonka 64 viestin jono on täynnä, katkaistaan saman resync-viestin jälkeen: se ei koskaan viivästytä toimintoa. Virta ilmoittaa 3 sekunnin uudelleenyhdistämisviiveen.

Välityspalvelimen kautta. : ping-kommentti lähetetään 25 sekunnin välein, jotta välityspalvelin ei katkaise hiljaista virtaa; X-Accel-Buffering: no pyytää nginxiä olemaan puskuroimatta sitä. Virtaa ei pakata, se välttää tavallisten pyyntöjen aikakatkaisun ja lasketaan nopeusrajoituksessa vain yhdeksi pyynnöksi. Taustaprosessin pysäytys sulkee kaikki virrat; pysäytyksen aikana pyydetty tilaus saa vastauksen 503.

Useita instansseja. SQLitellä tietokantaa pitää hallussaan yksi instanssi: muistissa oleva väylä riittää. PostgreSQL:llä jokainen instanssi välittää toimintonsa muille LISTEN/NOTIFY-mekanismilla kanavalla blunderdb_events, heti kun --direction tai --transcription on käytössä: yhteen instanssiin yhdistetty tilaaja kuulee toisessa instanssissa vahvistetun toiminnon tai saman tietokannan kautta call-komennolla tehdyn toiminnon. Tenant kulkee ilmoituksen mukana, ja sen vastaanottava instanssi toimittaa sen vain kyseisen tenantin tilaajille. Jokainen instanssi avaa kaksi lisäyhteyttä (application_name blunderdb-events-… kuuntelua varten, blunderdb-notify-… lähettämistä varten); instanssi, joka ei voi kuunnella käynnistyessään, kieltäytyy käynnistymästä. call ilmoittaa kuuntelematta ja palvelee pyyntönsä, vaikka se ei voisi ilmoittaa.

Mikä tahansa yhdistämiseen oikeutettu rooli voi lähettää tällä kanavalla, myös --rls-asetuksella. Vastaanotettu ilmoitus uskotaan vain, jos sen tenant on kelvollinen ja laji tunnettu; loput kirjataan lokiin ja ohitetaan. Väärennetty ilmoitus voi pahimmillaan saada tenantin tilaajat lukemaan tietonsa uudelleen.

  • Ilmoitus lähetetään tietokantakirjoituksen jälkeen, kuten paikallinen viestikin. Kaksi menetystä jää ilman resync-tapahtumaa: kirjoituksen ja ilmoituksen välillä lopetettu instanssi sekä pysäytys, joka ei ehdi lähettää jonossa jäljellä olevaa 2 sekunnissa. Toiminto on vahvistettu, mutta muissa instansseissa jo auki olevat virrat saavat siitä tiedon vasta, kun niiden asiakas yhdistyy uudelleen.

  • Katkennut kuunteluyhteys muodostetaan uudelleen kasvavalla odotusajalla 250 ms:sta 30 s:iin. Muiden instanssien katkon aikana tekemät toiminnot menetetään: yhteyden palautuessa jokainen instanssin tilaaja saa resync-viestin, jonka syy on missed. PostgreSQL:lle liian pitkä ilmoitus (8 000 tavua) tai ilmoitus, jota instanssi ei ole voinut lähettää, saapuu muille samana resync-viestinä kyseiselle tenantille.

  • Virran id-arvot ovat kunkin instanssin omia. Asiakas, jonka kuormantasaaja ohjaa toiseen instanssiin, ei hyödy niistä: jokaisen virran avaava resync saa sen lukemaan uudelleen sen, mitä se näyttää.

Bearoff-tietokannat

Demoni laskee kaksi oletustaulukkoaan käynnistyksessä taustalla (TS-06-06 kuutiotuomiolle, OS-06 EPC:lle): noin kuusi sekuntia yhdellä ytimellä, kerran, datakansiossaan — $XDG_DATA_HOME/blunderdb, tai sen puuttuessa ~/.local/share/blunderdb. Mitään ei ladata eikä mitään ole upotettu binääriin (ADR-0027). Jos kansio on vain luettavissa, taulukot pidetään muistissa prosessin eliniän ajan: palvelu käynnistyy, se vain maksaa laskennan jokaisella uudelleenkäynnistyksellä.

Laajempaa aluetta ei lasketa käynnistyksessä — TS-06-11 painaa 1,2 Gt ja vie minuutteja, eikä palvelu päätä sellaisesta yksin. Ylläpitäjän tehtävä on tehdä se komentoriviltä siihen levyniteeseen, jota taustapalvelu lukee:

# generate
blunderdb bearoff generate --ts 6x11 --data-dir /srv/data/blunderdb

# serve
XDG_DATA_HOME=/srv/data blunderdb serve --db database.db
blunderdb serve --db database.db \
    --bearoff-ts /srv/data/blunderdb/gnubg_ts6x11.bd

Ensimmäisessä käynnistyksessä demoni etsii taulukon itse datakansiostaan; toisessa se osoitetaan polulla, sijaitsipa se missä tahansa. --data-dir on bearoff-alikomentojen valitsin, ei koskaan serve-komennon.

blunderdb bearoff list --data-dir /srv/data/blunderdb kertoo mitä levynide sisältää ja mitä kukin alue maksaisi; blunderdb bearoff verify päättyy virheeseen vioittuneesta taulukosta, mikä tekee siitä sellaisenaan käyttökelpoisen käynnistystarkistuksen. Yksityiskohdat: Komentoriviliittymä (CLI).

Ylläpitoreitit

Kaksi kutsua ei pysähdy niitä tekevään tenanttiin, ja ne elävät siksi omalla etuliitteellään, POST /ops/<perhe>.<metodi>:

  • /ops/maintenance.vacuum (SQLite-taustajärjestelmä) kirjoittaa koko tiedoston uudelleen, kaikkien tenanttien tiedot mukaan lukien, ja pitää kirjoituslukkoa koko ajan;

  • /ops/tenant.purge (PostgreSQL-taustajärjestelmä) tuhoaa tenantin tiedot, ja tuhottava tenant on se, jonka nimeää kutsujan hallitsema otsake.

Palvelin ei tunnista ketään (ks. alla): reitti, johon yksi tenant pääsee, on reitti, jota jokainen tenant voi kutsua. Etuliite on olemassa, jotta välityspalvelin voi torjua molemmat yhdellä säännöllä. Älä koskaan altista /ops/ -reittejä julkiselle välityspalvelimelle. nginxissä sääntö mahtuu yhdelle riville server-lohkossa; Caddyssä kahdelle riville sivuston määrittelyssä:

location /ops/    { return 403; }
location /metrics { return 403; }
@closed path /ops/* /metrics
respond @closed 403

Valitsin --ops-addr <isäntä:portti> menee pidemmälle: kaksi reittiä poistuvat tällöin --addr-osoitteesta ja niitä tarjotaan vain tuossa toisessa kuuntelijassa, joka sidotaan hallintaliitäntään. Ilman valitsinta ne pysyvät pääkuuntelijassa ja niiden estäminen on välityspalvelimen asia.

Nämä reitit vaativat X-Tenant-ID-otsakkeen kuten kaikki muutkin — tyhjennys nimeää tenantin, jonka se tuhoaa, ja tarvitsee otsaketta enemmän kuin mikään muu. Vain koettimet (/healthz, /readyz) ja /metrics pärjäävät ilman.

Siksi yllä oleva estosääntö kattaa myös polun /metrics: koska se ei vaadi mitään tenanttia, sen voi lukea kuka tahansa, joka tavoittaa demonin, ja se julkaisee tietokannan koon ja käynnissä olevan työn kaikkien tenanttien osalta. Sitä katsotaan demonin omalta koneelta tai polusta, jonka välityspalvelin varaa ylläpidolle. Kolmas kohta, jota ei saa koskaan julkaista, ei ole reitti vaan kuuntelija: valitsimen --pprof-addr kuuntelija, joka ei tunne tenantteja lainkaan ja luovuttaa profiilin koko prosessista. Se sidotaan hallintaliitäntään, eikä välityspalvelin julkaise sitä koskaan.

Mikä ei siirtynyt /ops/-etuliitteen alle: /v1/gammonnet.sweepStale. Täydennysajo on kallis mutta rajattu kutsuvaan tenanttiin; sitä rajoittavat nopeusraja ja käynnissä olevan työn mittarit, ei luottamusraja.

Koko sopimus — jokainen metodi, sen pyyntö ja vastaus — tuotetaan lähdekoodista ja versioidaan: openapi.yaml arkiston juuressa (OpenAPI-muoto, skeemat mukaan lukien) ja sen luettava liite API-sopimus (yksi taulukko perhettä kohden). Molemmat luodaan uudelleen komennolla go run ./cmd/openapi-gen, ja oma testinsä epäonnistuu, jos jompikumpi jää jälkeen todella rekisteröidyistä reiteistä.

Jokainen /v1-pyyntö hyväksyy JSON-rungon (Content-Type: application/json, tai ei otsaketta lainkaan — muun tyyppinen runko hylätään koodilla 400 invalid sen sijaan, että se epäonnistuisi hämmentävään JSON-jäsennysvirheeseen); tunnettu metodi, jota kutsutaan väärällä HTTP-verbillä, vastaa 405, ja Allow-otsake nimeää ainoan hyväksytyn verbin. Listausmetodit, jotka hyväksyvät limit-parametrin, hylkäävät yli 1000 riviä sivua kohti (400 invalid) sen sijaan, että kunnioittaisivat rajoittamatonta arvoa.

Jokainen listaava perhe hyväksyy limit- ja offset-parametrit: positions.list, positions.listIds, matches.list, search.find, anki.reviewLog, comments.listAll, tournaments.list ja collections.positions. Molemmat ovat oletuksena nolla, mikä tarkoittaa samaa kuin aina: kaikki. Implisiittistä kattoa ei ole — virtaa ei pidetä muistissa, joten rajaamaton lista maksaa aikaa ja kaistaa muttei koskaan taustapalvelun tasapainoa, kun taas hiljainen oletusraja saisi asiakkaan lukemaan katkaistun listan täydellisenä. Nämä kaksi parametria antavat mahdollisuuden sivuttaa, sille joka sitä haluaa.

Jokainen TCP-yhteys on aikarajoitettu luku-/kirjoitusajan osalta per pyyntö — anteliaampi budjetti tavallisille kutsuille, paljon suurempi suoratoistoreiteille (NDJSON-listat, tuonnit/viennit, gammonNet-kiinnikuromispyyhkäisy) — ja niiden samanaikaisten avointen yhteyksien määrä on rajattu: sen ylittyessä uusi yhteys odottaa, että jokin olemassa olevista vapautuu, sen sijaan että jokainen yhteys saisi ehdoitta oman suoritussäikeensä. Hallittu sammutus (SIGINT/SIGTERM) peruu ensin jokaisen käynnissä olevan tuonnin ja gammonNet-kiinnikuromispyyhkäisyn — kukin vastaa lopuksi tapahtumalla {"event":"cancelled"} sen sijaan, että sen yhteys katkaistaisiin selittämättä — ennen palvelimen sulkemista tavanomaisen armonajan kuluessa. Ladatun tuonnin väliaikainen tiedosto säilyttää alkuperäisestä tiedostopäätteestä vain ne, jotka taustaprosessi tunnistaa (.xg, .xgp, .sgf, .mat, .bgf, .ogxm, .txt, .db, .dbx), ja kaikki samanaikaiset tuonnit — kaikkien tenanttien kesken — jakavat yhden yhteisen levylle puskuroitujen tavujen kiintiön: sen ylittyessä uusi tuonti hylätään (too many requests) sen sijaan, että $TMPDIR-käyttö kasvaisi rajattomasti.

/v1/imports.json lukee blunderDB:n JSON-viennin takaisin täyttäen aukot: sen sisältämä analyysi kirjoitetaan vain asemaan, jolla ei vielä ole analyysiä, korvaamatta koskaan olemassa olevaa, ja molempien puolten rolloutit säilytetään.

search-perhe tarjoaa kolme ovea samaan hakuun. search.find ottaa täyden suodatinolion, kenttä kentältä. search.query ottaa kyselyn, joka on kirjoitettu sovelluksen komentorivin kielellä (s cube p>30 E>50, kuvattu sivulla Komentoluettelo), ja suoratoistaa samat asemat; se on ainoa tapa tavoittaa verkon yli ne suodattimet, joilla ei ole ilmeistä kenttää — siirtokuvio, kommenttiteksti, pelaaja, päivämäärä, poissuljetut heitot, vyöhykkeet ja blotit. search.parse ei hae mitään: se vastaa, mitä kysely tarkoittaa — suodattimet joita se merkitsee, kanonisen muotonsa (kaksi samaa tarkoittavaa kyselyä jakavat sen, mikä tekee tallennetusta hausta vertailukelpoisen) ja diagnostiikkansa.

Kysely, jossa on tunnistamaton osanen, hylätään (400 invalid, osanen nimettynä) sen sijaan että se suoritettaisiin kaventaen hakua hiljaisesti. Osanen, joka ymmärretään mutta jolla ei ole täällä vaikutusta — x, joka kytkee päälle poissulkevan rakenteen, joka on lauta eikä tekstiä — kulkee otsakkeessa X-BlunderDB-Query-Diagnostics, jotta runko pysyy asemien NDJSON:na kaikille nykyisille asiakkaille.

Kaksi positions-perheen metodia purkaa aseman tallentamatta sitä: positions.fromXGID rakentaa aseman XGID-merkkijonosta ja positions.fromXGP yksittäisen aseman .xgp-tiedostosta.

POST /v1/exports.sqlite vie koko nykyisen tenantin — asemat, kokoelmat, ottelut, turnaukset, analyysit, kommentit, pelatut siirrot, suodatinkirjaston ja Anki-pakat — SQLite-tiedostoon, jonka työasema voi avata sellaisenaan. Pyynnön JSON-runko on valinnainen: watermarkOrigin / watermarkNote lisäävät vesileiman, jonka allekirjoittaa demonin oma identiteetti (--identity-dir) — ilman näitä kenttiä vienti ei kanna vesileimaa; niiden pyytäminen ilman määritettyä identiteettiä epäonnistuu invalid-virheellä. collectionIds rajaa viennin näihin kokoelmiin ja niiden asemiin, analyyseineen, kommentteineen ja pelattuine siirtoineen, ilman suodatinkirjastoa ja Anki-pakkoja.

Kokoelman jakaminen tenantien välillä kulkee asiakkaan kautta, ei koskaan lukemalla toisesta tenantista toiseen: antava tenant kutsuu exports.sqlite-menetelmää parametrilla collectionIds (ja vesileimalla, jotta vastaanottaja tietää, mistä tiedosto tulee), vastaanottava tenant lähettää tiedoston menetelmälle imports.db. Jokaisella pyynnöllä on oma X-Tenant-ID; välityspalvelin (proxy) päättää, kuka saa tehdä kumpaakin. Tuonnissa kokoelma liittyy vastaanottajan samannimiseen kokoelmaan tai luodaan; sen asemat lisätään loppuun ilman kaksoiskappaleita. Vastaanottajan elävä kokoelma ei saa yhtään asemaa: sen kysely määrää sen sisällön. Tietokannan tuonti työpöytäsovelluksessa noudattaa samaa sääntöä.

curl -X POST http://127.0.0.1:8080/v1/exports.sqlite \
     -H 'X-Tenant-ID: club-lyon' -H 'Content-Type: application/json' \
     -d '{"collectionIds":[4],"watermarkOrigin":"Club de Lyon"}' -o ouvertures.db
curl -X POST http://127.0.0.1:8080/v1/imports.db \
     -H 'X-Tenant-ID: alice' -F file=@ouvertures.db

training-perhe pitää Harjoittelu-välilehden lokia: training.save lisää harjoituskerran (exercise, seedSource, lukumäärät, items) ja palauttaa sen id:n (Idempotency-Key hyväksytään); training.sessions lukee harjoituskerrat uudelleen, tuorein ensin (exercise ja limit valinnaisia); training.numberStats kokoaa harjoituksen itemit lukutyypin mukaan. Kysymykset sen sijaan arpoo asiakas.

gammonnet.evaluate arvioi paljaan aseman (position tai xgid) lukematta tai kirjoittamatta mitään tenantissa: nopilla parhaat siirrot (candidates, oletuksena 5, enintään 20); ilman noppia tuplauspäätös. ply vaihtelee välillä 0–2 (oletuksena 2); syvempi haku on analyzeMissing-menetelmän työtä.

anki-perhe saa kuusi uutta metodia, jotka laajentavat aikaväliin perustuvaa kertausajastinta (FSRS): anki.reviewLog (loki jokaisesta kertauksesta — arvosana ja FSRS-tulos — säilytystilastoja ja tarkkaa historiaa varten), anki.forecast (ennuste erääntyvien korttien määrästä tulevina päivinä, myöhässä olevat kortit mukaan lukien), anki.suspendCard / anki.buryCard / anki.removeCard (kortin poistaminen kertausjonosta väliaikaisesti tai pysyvästi) sekä anki.retention (pakan kertauksista mitattu onnistumisaste, luettuna suhteessa sen omistajan asettamaan tavoitteeseen).

Muista

anki.retention korvaa metodin anki.optimizeParams, joka siirsi tavoitetta kohti havaittua astetta ja saattoi kirjoittaa sen. Säilytystavoite on valinta kuormituksen ja laadun välisestä kompromissista, mitattu aste on sen tulos, ja toisen kytkeminen toiseen on juuri se mekanismi, jonka FSRS:n tekijät hylkäävät. Metodi vain mittaa, eikä koskaan kirjoita.

Perhe stats tarjoaa metodin stats.playerTable, joka palauttaa yhden tilastorivin pelaajaa kohden (ottelut, voitot/tappiot, lasketut päätökset, PR yhteensä / nappulat / kuutio, Snowie Error Rate, virheet, karkeat virheet ja tuuri) niistä otteluista, jotka annettu suodatin säilyttää. Kuten graafisessa käyttöliittymässä, taulukko noudattaa suodattimesta vain ajanjaksoa, turnauksia ja otteluiden pituutta: pelaajavalinta ja päätöstyyppi ohitetaan, sillä taulukko koskee kaikkia pelaajia ja erittelee nappulat ja kuution jo omiin sarakkeisiinsa. Kenttä luck_known kertoo, mitattiinko tuuri kyseiselle pelaajalle; kenttää luck_rate_mp ei pidä lukea, kun se on false — tuntematon tuuri ei ole nollatuuri.

stats-metodeille annettu suodatin hyväksyy PlayerName-kentän rinnalla kentän PlayerAliases: muut kirjoitusasut, joilla sama henkilö on esiintynyt. Koska pelaajan nimi kirjoitetaan käsin jokaiseen tiedostoon, sama henkilö esiintyy usein useassa asussa, ja suodatin, joka säilyttää vain yhden, laskee osasta otteluita ilman että mikään näyttää oudolta. Kenttä on puhtaasti additiivinen: minkä tahansa nimen päätökset säilytetään. Nimien yhdistäminen tietokannassa (MergePlayers) on toinen vastaus, ja se kannattaa varata tietokantoihin, joita ei ole saatu joltakulta toiselta — se kirjoittaa kaikkien ottelut uusiksi.

Kaksi metodia täydentää yhdenvertaisuuden graafisen käyttöliittymän kanssa: stats.tournamentBadges palauttaa kunkin tietokannan turnauksen kortilla näkyvän luvun (viitepelaajan PR), ja matches.findByHash kertoo kahden kaksoiskappaleiden tunnistussormenjäljen perusteella, onko annettu ottelu jo tallessa — sen verran, että turhan tuonnin voi välttää ennen sen aloittamista.

Pelin winner-kentällä, jonka matches.createGame vastaanottaa ja matches.games palauttaa, on vain yksi koodaus: 1 pelaajalle 1, -1 pelaajalle 2, 0 keskeneräiselle pelille. Asiakas, joka lähettää yhä arvoja 0, 1 tai -1 gnubgin merkityksessä (0 pelaajalle 1, 1 pelaajalle 2), tallentaa päinvastaisen voittajan.

analyses.repair laskee analyysin denormalisoidut sarakkeet (mukaan lukien cube_error) uudelleen sen täydestä analyysistä ja palauttaa todella korjattujen rivien määrän. Nämä sarakkeet ovat vain projektio: projektiovirhe korjautuu siis ilman lähdetiedostojen uudelleentuontia. Toiminto on nimenomainen eikä käynnisty koskaan itsestään — ei tietokantaa avattaessa eikä migraation yhteydessä, sillä skeema ei ole syyllinen. Lukukelvoton analyysi jätetään sellaisekseen sen sijaan, että se nollattaisiin. Tunnettu tapaus: gnuBG:n ”Double No” -merkinnällä varustetut ei-tuplaukset, jotka luettiin väärin ennen versiota 0.33.0 ja jotka kantoivat koskaan tapahtumattoman tuplauksen virhettä.

gammonnet.analyzeMissing käynnistää nykyisen tenantin gammonNet-täydennysanalyysin: kirjoittaa analyysin jokaiselle asemalle, jolla ei ole yhtään (ADR-0013, ADR-0015). Kyseessä on kirjasto-operaatio — se lukee ja kirjoittaa tallennettuja asemia ja analyysejä — ei koskaan paljas evaluaattori: blunderdb serve toimii kirjastolla, gammonnet serve evaluoi aseman. Vastaus on NDJSON-virta (started, progress, sitten done tai error/cancelled) samaan tapaan kuin tuonnin päätepisteissä; gammonnet.analyzeMissing.cancel (started-tapahtumassa saadulla job_id:llä) peruuttaa käynnissä olevan täydennysanalyysin ja toimii yhtä lailla sekä täydennysanalyysille että uudelleenanalyysille (alla). Kyseessä on sama toiminto kuin tuonnin jälkeinen automaattinen käynnistys ja graafisen käyttöliittymän nimenomainen ele sekä alikomento blunderdb analyze (katso Komentoriviliittymä (CLI)) — kolme muotoa, yksi logiikka.

gammonnet.sweepStale on analyzeMissing:n vastine uudelleenanalyysille täydennyksen sijaan: jokainen positio, jonka analyysi on kokonaan gammonNetin omaa mutta vanhentunut — nyt käynnissä olevaa vanhempi moottoriversio, tai ply:stä poikkeava syvyys — arvioidaan uudelleen pyydetyllä syvyydellä. Vanhentumiskriteeri on jaettu graafisen käyttöliittymän saman erän ja blunderdb analyze --stale:n kanssa (ei kahdennettua logiikkaa kolmen tilan välillä); positiota, jolla on XG-, GNUbg- tai BGBlitz-analyysi, ei koskaan koske, riippumatta sen gammonNet-sisällöstä — ADR-0013:n suoja pysyy ehdottomana. Sama NDJSON-muoto kuin analyzeMissing:llä, ja kummankin reitin lopputapahtuma kantaa jaon evaluated/refused/failed: positio, jota gammonNet kieltäytyy arvioimasta (ottelutulos sen taulukon kattavuuden ulkopuolella, mallin hylkäämä tuplauspäätös), lasketaan refused-arvoksi, ei failed-arvoksi — sitä ei koskaan yritetä turhaan uudelleen seuraavalla kierroksella, toisin kuin todella epäonnistunutta positiota.

rollout.position pelaa kirjaston aseman (positionId) rolloutilla ja palauttaa jokaiselle ehdokkaalle ekvityn, sen 95 %:n välin ja JSD:n; rollout sisältää asetukset (fast, standard tai standard,ply=1…), store tallentaa valmiin rolloutin toisena analyysina aseman oman analyysin viereen, jota se ei koskaan korvaa. Pelkkä asema (XGID) hylätään: daemon toimii kirjastolla. rollout.filter on komennon blunderdb analyze --rollout erämuoto: query-kentän (haun kieli) valitsemat asemat, joilla ei vielä ole rolloutia samoilla asetuksilla, pelataan yksi kerrallaan ja tallennetaan sitä mukaa NDJSON-virtana (started, progress jokaisen pelisarjan jälkeen, sitten done, cancelled tai quota_exceeded); rollout.filter.cancel peruu sen job_id-tunnisteella. Tenant ajaa vain yhtä erää kerrallaan, rolloutia tai gammonNetiä. rollout.list lukee aseman tallennetut rolloutit.

Korrelaatio ja liiketoimintamittarit

Jokainen pyyntö saa korrelaatiotunnisteen: sen, jonka asiakas (tai käänteisvälityspalvelin) lähettää X-Request-Id-otsakkeessa, muutoin luodun — kummassakin tapauksessa se palautetaan vastauksen samassa otsakkeessa ja lisätään pyynnön päättävään lokiriviin (kenttä request_id). Mahdollinen traceparent (W3C Trace Context) välitetään sellaisenaan samaan lokiriviin — palvelin ei jäsennä eikä tarkista sitä eikä sisällä mitään jäljityskirjastoa: se on silta näiden lokien yhdistämiseksi ylävirrassa toimivaan jäljitysketjuun, ei muuta.

Pyyntömäärän ja viiveen lisäksi /metrics julkaisee mittarit käynnissä olevasta työstä, joka muutoin jäisi näkymättömäksi jumittuneessa tuonnissa tai gammonNet-erässä (yksi hyvin pitkä pyyntö, ei monta pyyntöä):

  • blunderdb_imports_inflight — käynnissä olevat tuonnit, kaikki tenantit yhteensä;

  • blunderdb_import_spool_bytes — tuontipuskurin kiintiöstä tällä hetkellä varatut tavut (ks. --rate-limit-* edellä pyyntöä sekunnissa koskevaa vastinetta varten);

  • blunderdb_gammonnet_sweep_inflight — käynnissä olevat gammonNet-täydennysajot, kaikki tenantit yhteensä;

  • blunderdb_database_size_bytes — pääasiallisen SQLite-tiedoston koko tai pg_database_size PostgreSQL:ssä (koko tietokanta, ei tenantkohtaisesti, kuten alla olevat yhteysvarannon mittarit); puuttuu, kunnes ensimmäinen mittaus on julkaistu.

Prosessin muisti- tai suoritinprofiili saadaan käynnistämällä valitsimella --pprof-addr <isäntä:portti> (net/http/pprof): oletuksena pois päältä ja tarkoituksella eri osoitteessa kuin --addr, koska nämä päätepisteet eivät tunne tenantteja.

Virtojen pakkaus

NDJSON-listaukset toistavat samat kenttien nimet joka rivillä. Palvelin pakkaa ne, kun asiakas sen hyväksyy: lähetä Accept-Encoding: gzip, ja vastaus palaa muodossa Content-Encoding: gzip. Mitattu ottelulistauksella: 13,5 % alkuperäisestä koosta tuhannella rivillä, 14,6 % sadalla.

Pakkaus ei muuta virran vaiheittaisuutta — jokainen tietue lähtee asiakkaalle kuten ennenkin, vain pakattuna matkalla. Se koskee vain NDJSON-, JSON- ja tekstivastauksia: tietokannan vienti tai .dbx-säiliö on jo pakattu, ja uudelleenpakkaaminen vain kasvattaisi sitä. Accept-Encoding: gzip;q=0 kieltää sen nimenomaisesti.

Vain yksi tenant SQLitessä

SQLite-taustajärjestelmässä ei ole tenant-saraketta: kaikki tiedot ovat samoissa tauluissa ilman väliseinää. Siksi palvelin torjuu tässä taustajärjestelmässä kaikki muut X-Tenant-ID-arvot kuin 1 — muiden hyväksyminen tarkoittaisi kaikkien rivien tarjoamista jokaiselle otsakkeen takana, joka väittää toisin. Aidosti monta tenanttia tarvitseva asennus tarvitsee PostgreSQL-taustajärjestelmän.

Useiden tenanttien lukeminen

Valmentaja, joka lukee oppilaidensa otteluita, tai seura, joka jakaa kirjaston: näiden tilien välinen suhde on niitä todentavalla isännällä, ei koskaan demonissa. Välityspalvelin ilmaisee sen X-Read-Tenants-otsakkeella, pilkuilla erotetulla tenanttien luettelolla (X-Read-Tenants: 2, 3), jonka se asettaa X-Tenant-ID:n viereen. Demoni luottaa siihen kuten X-Tenant-ID:hen eikä valtuuta itse mitään (ADR-0063).

Toiminto on oletuksena pois käytöstä, ja pois käytöstä tarkoittaa hylättyä: kunnes demoni käynnistetään valitsimella --read-tenants (tai BLUNDERDB_READ_TENANTS=true; Config.TrustReadTenants moottorin upottavalle isännälle), jokainen pyyntö, jossa on ei-tyhjä X-Read-Tenants, hylätään (400) reitistä riippumatta. Ota se käyttöön vasta, kun välityspalvelin on määritetty poistamaan kaikki asiakkaan lähettämä arvo ja asettamaan luettelo itse.

Vain /v1/across.*-lukukutsut katsovat tätä otsaketta. Koko luettelo: across.searchFind, across.matchesList, across.statsCompute ja across.playerTable; ne lukevat ensin X-Tenant-ID:n, sitten jokaisen luetellun tenantin otsakkeen järjestyksessä, yhteensä enintään 64 eri tenanttia. Luettelossa olevalla tenantilla, nimettynä id:llä: across.matchesGet, across.matchMovePositions (ottelun positiot siirto siirrolta) ja across.analysesLoadByIds; luettelosta puuttuva tenantti hylätään niissä. Jokainen tulos kantaa alkuperäisen tenanttinsa ("tenant": "2"), koska id on yksilöllinen vain omassa tenantissaan; positio kantaa myös Zobrist-tiivisteensä ("zobrist"), joka tarkoittaa samaa lautaa kaikissa tenanteissa. limit koskee jokaista tenanttia; 0 tarkoittaa 1000, ja suurempi arvo hylätään. NDJSON-virrassa myöhäisen tenantin virhe tulee viimeisenä rivinä jo luettujen tenanttien tulosten jälkeen: koko virta epäonnistuu silloin.

curl -s http://127.0.0.1:8080/v1/across.matchesList \
  -H 'X-Tenant-ID: 1' -H 'X-Read-Tenants: 2, 3' -d '{"limit":20}'

Jokainen kirjoitus pysyy X-Tenant-ID:ssä: mikään muu reitti ei lue X-Read-Tenants-otsaketta. Ilman otsaketta across.*-luku koskee vain X-Tenant-ID:tä. Virheellinen otsake (nimi, tyhjä alkio, yli 64 tenanttia) tai usealla rivillä lähetetty otsake hylkää koko pyynnön reitistä riippumatta. SQLitessä, jossa on vain yksi tenantti, luettelossa voi olla vain 1: otsake ei laajenna siellä mitään. Nämä reitit kuuluvat vain palvelimelle: työpöytäsovelluksella ja call-komennolla on vain yksi tenantti.

across.*-pyyntö maksaa jopa 64 lukua tallennuksesta, mutta nopeusrajoitus (--rate-limit-rps) laskee sen vain kerran, X-Tenant-ID:lle: mitoita tietokanta ja tämä raja sen mukaan tai anna välityspalvelimen rajata luettelo. across.*-reitin käyttöloki sisältää vastaanotetun luettelon (kenttä read_tenants). Otsake ei kuulu sallittuihin CORS-otsakkeisiin: vain välityspalvelin kirjoittaa sen, ei koskaan selain.

Varmuuskopiointi ja palautus

Neljä tapaa, sen mukaan mitä halutaan palauttaa.

Kaikki, PostgreSQL:n alla — pg_dump on työkalu, eikä blunderDB:llä ole siihen lisättävää:

pg_dump --format=custom --file=blunderdb.dump "postgres://…"
pg_restore --dbname="postgres://…" blunderdb.dump

Kaikki, kontissa ajettavan SQLiten kanssa — tiedosto avataan WAL-tilassa (demoni koodaa yhteysmerkkijonoonsa journal_mode(WAL) kaikille poolin yhteyksille): tiedoston blunderdb.db rinnalla elävät -wal ja -shm, ja tuoreimmat kirjoitukset ovat -wal-tiedostossa. Pelkän .db-tiedoston kopioiminen käynnissä olevalta demonilta antaa siis epätäydellisen tiedoston, eikä mikään kerro siitä. Kaksi turvallista tapaa:

  • pysäytä demoni ja kopioi sitten koko levynide — pysäytettynä kaikki kolme tiedostoa ovat keskenään yhtenäisiä, ja varmuuskopioinnin yksikkö on levynide, ei pelkkä .db;

  • älä kopioi tiedostoa lainkaan: /v1/exports.sqlite (alla) kirjoittaa täydellisen .db-tiedoston demonin ollessa käynnissä, ja se on ainoa tapa, joka ei vaadi minkäänlaista katkosta.

/ops/maintenance.vacuum kyllä kokoaa WAL:n päätiedostoon ennen sen uudelleenkirjoitusta, mutta se ei jäädytä tietokantaa: seuraava kirjoitus menee taas WAL:iin. Se on tiivistyskomento, ei varmuuskopiointimenetelmä.

Yksi tenant erikseen — /v1/exports.sqlite kirjoittaa tenantin tietokannan tavalliseen .db-tiedostoon, samaan jonka työpöytäsovellus avaa:

curl -X POST http://127.0.0.1:8080/v1/exports.sqlite \
  -H "X-Tenant-ID: 42" -o tenant-42.db

Tämä komento ajetaan demonin omalla koneella: se kohdistuu paikalliseen kuuntelijaan, ohittaa välityspalvelimen ja asettaa siksi tenant-otsakkeen itse. Ulkopuolelta kysellään välityspalvelinta, ja tenant on todennetun tilin tenant — otsaketta ei anneta, sillä välityspalvelin poistaa asiakkaan otsakkeen ennen kuin lisää omansa:

curl -u alice:… -X POST \
  https://blunderdb.example.com/v1/exports.sqlite -o tenant-alice.db

Tiedoston palauttaminen — migrate kopioi sen haluttuun tenanttiin:

./blunderdb migrate --from tenant-42.db --to "postgres://…" --tenant-id 42

migrate kieltäytyy kirjoittamasta tenanttiin, jossa on jo jotain, ja kertoo mitä (”128 asemaa, 3 ottelua”); --on-conflict skip jatkaa siitä huolimatta ja antaa Zobrist-kaksoiskappaleiden poiston yhdistää asemat.

Mitä migrate ei kopioi ja mistä se ilmoittaa lopuksi tarkan luvun kanssa: Anki-pakat ja niiden kortit, suodatinkirjaston, haku- ja komentohistoriat sekä istunnon tilan. Nämä ovat työpöytäsovelluksen käyttötietoja; asemat, joihin ne viittaavat, on kyllä siirretty.

Virheen ja blunderin kynnykset sen sijaan kopioidaan: ne eivät ole käyttödataa vaan lukutapa, josta laskut riippuvat, ja tenant, joka laskisi toisin kuin tiedosto, josta se on peräisin, tekisi migraatiosta äänettömän merkityksen muutoksen.

Tenant säätää omansa kutsuilla POST /v1/librarySettings.load ja /v1/librarySettings.save. Toisin kuin metadata, joka on vain luettavaksi avattu globaali infrastruktuuri, asetustaulu kantaa tenant_id-saraketta ja elää Row-Level Securityn alla: tenant, joka kirjoittaa kynnyksensä, yltää vain omiin riveihinsä.

Työasema ja palvelin

Työpöytäsovellus avaa .db-tiedostoja, ei URL-osoitteita: se ei ota yhteyttä mihinkään serve-demoniin, eikä missään ole kenttää, johon syöttää osoite. Palvelin ja työasema vaihtavat tiedostoja kahdella symmetrisellä tavalla:

Tenanttien välistä lukemista ei ole lainkaan. Eristys on täydellinen: mikään, minkä yksi tenant tallentaa, ei näy toiselle millään reitillä, eikä yksikään kutsu ota tenanttia parametrina — jokainen pyyntö tuntee vain sen, jonka välityspalvelin on siihen asettanut. Valmentajalla, joka haluaa nähdä oppilaidensa ottelut, on siis kaksi tietä, molemmat nimenomaisia:

  • avataan hänelle välityspalvelimeen lisätili, joka on liitetty oppilaan tenanttiin: välityspalvelimen vastaavuustaulukko, ei koskaan demoni, ratkaisee minkä tenantin istunto näkee;

  • pyydetään häneltä vienti — exports.sqlite-reitin tai työpöytäsovelluksen vienti-ikkunan tuottama .db — ja avataan se omalla koneella.

Käyttöönotto Dockerilla

Arkisto tarjoaa Dockerfile.serve-tiedoston, joka rakentaa demonista minimaalisen konttikuvan: vain serve-binääri käännetään (puhdas Go, ilman graafista käyttöliittymää ja ilman CGO:ta, siis staattisesti linkitetty) ja sijoitetaan sitten distroless-kuvaan.

# build
docker build -f Dockerfile.serve -t blunderdb-serve .

# run
docker run --rm -p 127.0.0.1:8080:8080 \
    -e BLUNDERDB_DSN="postgres://user:pass@host:5432/blunderdb?sslmode=disable" \
    blunderdb-serve

Rakentaminen käynnistetään repositorion juuresta, ja kuvan oletustaustajärjestelmä on postgres.

Kuva kuuntelee porttia 8080 ja konfiguroidaan ympäristömuuttujilla (BLUNDERDB_BACKEND, BLUNDERDB_DSN, BLUNDERDB_ADDR, BLUNDERDB_RLS). Se määrittelee HEALTHCHECK-tarkistuksen, joka suorittaa 30 sekunnin välein blunderdb healthcheck (pyyntö osoitteeseen /readyz — distroless-kuvassa ei ole curl-ohjelmaa eikä komentotulkkia): docker ps näyttää kontin tilan healthy tai unhealthy, ja Compose tai orkestroija voivat odottaa taustaprosessin valmiutta ennen siitä riippuvien osien käynnistämistä.

Julkaistu kuva

Kuvaa ei tarvitse rakentaa itse: jokainen julkaistu blunderDB-versio työntää omansa GitHubin rekisteriin (GHCR) nimellä ghcr.io/kevung/blunderdb-serve. Käytettävissä on kaksi tunnistetta: versionumero, joka pysyy ikuisesti kiinnitettynä tähän kuvaan, ja latest, joka seuraa viimeisintä julkaistua versiota. Koko dokumentaatio merkitsee ne muodossa ghcr.io/kevung/blunderdb-serve:<version>: kohdan <version> tilalle tulee julkaistun version numero, ja juuri tämä muoto, ei koskaan latest, kiinnitetään tuotantokäyttöönotossa. Kuva tarjotaan arkkitehtuureille linux/amd64 ja linux/arm64; Docker valitsee isännän arkkitehtuurin.

# pull
docker pull ghcr.io/kevung/blunderdb-serve:<version>

# postgres
docker run --rm -p 127.0.0.1:8080:8080 \
    -e BLUNDERDB_DSN="postgres://user:pass@host:5432/blunderdb?sslmode=disable" \
    ghcr.io/kevung/blunderdb-serve:<version>

# sqlite
docker run --rm -p 127.0.0.1:8080:8080 \
    -v blunderdb-data:/data \
    -e BLUNDERDB_BACKEND=sqlite -e BLUNDERDB_DSN=/data/blunderdb.db \
    ghcr.io/kevung/blunderdb-serve:<version>

/data on kuvan valmistelema liitospiste, sen etuoikeudettoman käyttäjän oikeuksilla, ja samalla sen XDG_DATA_HOME: siihen liitetty levynide ei palvele vain tietokantaa, vaan bearoff-taulukot lasketaan sinne kerran, hakemistoon /data/blunderdb, ja löydetään uudelleen seuraavissa käynnistyksissä. Ilman levynidettä ne lasketaan uudelleen jokaisella kontin käynnistyksellä — muutamassa sekunnissa — ja demoni kertoo käynnistyksessä, jos se ei pysty kirjoittamaan niitä (could not prepare the bearoff tables; the exact regime will be unavailable), jolloin se palvelee normaalisti, bearoff-asemissa pelkällä arvioidulla tilalla.

Kuvassa on tavanomaiset OCI-tunnisteet (org.opencontainers.image.source, .version, .revision, .licenses): docker inspect kertoo, mistä commitista ja versiosta se on peräisin. Se rakennetaan jatkuvassa integraatiossa arkiston Dockerfile.serve-tiedostosta, täsmälleen kuten yllä; paikallinen rakentaminen tai julkaistun kuvan vetäminen tuottaa saman binäärin.

Varoitus

Kuten demoni itsekin, kontti ei suorita minkäänlaista todennusta (ADR-0005): se luottaa X-Tenant-ID-otsakkeeseen sellaisenaan kuin se sen vastaanottaa. Se on sijoitettava todennuksesta huolehtivan käänteisvälityspalvelimen taakse, joka asettaa tämän otsakkeen itse, eikä sitä saa koskaan altistaa suoraan julkiseen internetiin. Yllä olevat esimerkit julkaisevat portin osoitteessa 127.0.0.1 juuri tästä syystä, ja --addr sitoutuu samoin osoitteeseen 127.0.0.1: välityspalvelin on samalla koneella.

Käyttöönotto todentavan proxyn takana

ADR-0005 tekee käänteisvälityspalvelimesta demonin koko turvarajan: vain se todentaa kutsujan, vain sillä on oikeus asettaa X-Tenant-ID-otsake, ja sen on järjestelmällisesti poistettava kaikki asiakkaan lähettämä arvo ennen todennetun tenantin lisäämistä — muuten kuka tahansa voi esiintyä minä tahansa tenanttina pelkästään nimeämällä sen. Uhkamalli mahtuu yhteen lauseeseen: demoni olettaa luotetun sisäverkon, ja kuka tahansa, joka ottaa siihen suoraan yhteyden, on sen silmissä se tenant, joka se väittää olevansa. Repositorio tarjoaa täydellisen, sellaisenaan ajettavan esimerkin hakemistossa deploy/. Se elää git-repositoriossa, ei konttikuvassa: on siis kloonattava repositorio tai ladattava alla toistetut kaksi tiedostoa sekä deploy/.env.example samaan hakemistoon.

Compose-tiedosto asettaa Caddyn — esimerkinomainen HTTP Basic -todennus — palvelujen blunderdb-serve ja PostgreSQL eteen, Row-Level Security käytössä. Vain Caddy julkaisee portin: kaksi muuta palvelua elävät Docker-verkossa, joka on määritelty internal: true eikä siitä ole reittiä isäntäkoneeseen eikä internetiin, olipa myöhempi muutos lisännyt niille mitä ports:-määrityksiä tahansa.

deploy/docker-compose.yml
# Example deployment of `blunderdb serve` behind an authenticating reverse
# proxy — the security model ADR-0005 requires and, until now, that no example
# in this repository actually showed. See deploy/README.md for the threat
# model and doc/source/mode_headless.rst for the full walkthrough.
#
# Try it from the repository root:
#   POSTGRES_PASSWORD=changeme docker compose -f deploy/docker-compose.yml up -d --build
#   curl -u alice:demo-password http://localhost:8080/v1/metadata.counts -d '{}'
#   docker compose -f deploy/docker-compose.yml down -v

services:
  # Caddy is the ENTIRE security boundary (ADR-0005): it is the only service
  # with a published port, it authenticates every request, and it is the
  # only thing allowed to set X-Tenant-ID — see Caddyfile. Any reverse proxy
  # capable of stripping and re-setting a header works equally well; Caddy is
  # used here for its one-file config and built-in Basic Auth with no extra
  # modules. deploy/nginx-tenant-proxy.conf shows the equivalent nginx
  # snippet for an existing nginx deployment.
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "8080:80" # the ONLY port this compose project exposes to the host
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - edge # the published port lives here — "backend" is internal-only
      - backend
    depends_on:
      blunderdb-serve:
        condition: service_healthy

  # No `ports:` here — on purpose (ADR-0005). The daemon performs no
  # authentication of its own, so it must be reachable only from Caddy, over
  # the "backend" network, and never published to the host.
  blunderdb-serve:
    build:
      context: ..
      dockerfile: Dockerfile.serve
    restart: unless-stopped
    environment:
      BLUNDERDB_BACKEND: postgres
      BLUNDERDB_DSN: "postgres://blunderdb:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD, see deploy/.env.example}@postgres:5432/blunderdb?sslmode=disable"
      BLUNDERDB_ADDR: ":8080"
      # Row-Level Security: defence-in-depth *inside* the trust boundary
      # Caddy draws above — it does not replace the proxy (ADR-0005).
      BLUNDERDB_RLS: "true"
    volumes:
      # The bearoff tables are computed on first start and kept under
      # $XDG_DATA_HOME/blunderdb, which the image sets to /data: without a
      # volume they are recomputed at every restart of the container.
      - blunderdb-data:/data
    networks:
      - backend
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: blunderdb
      POSTGRES_USER: blunderdb
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD, see deploy/.env.example}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U blunderdb -d blunderdb"]
      interval: 5s
      timeout: 3s
      retries: 10

volumes:
  postgres-data:
  # Bearoff tables computed by blunderdb-serve on first start (see
  # XDG_DATA_HOME above): a few megabytes, worth keeping across restarts.
  blunderdb-data:
  caddy-data:
  caddy-config:

networks:
  # Caddy's own network, carrying the one published port. A container on
  # "backend" alone (blunderdb-serve, postgres) is never reachable through it.
  edge: {}
  # internal: true means this network has no route to the outside world and
  # accepts no published ports — blunderdb-serve and postgres can only ever
  # be reached by another container attached to it (here, only Caddy),
  # never from the host or the public internet, regardless of what `ports:`
  # a future edit might add to either service.
  backend:
    internal: true

Caddyfile todentaa, yhdistää todennetun tilin tenantin kokonaislukuun (map) ja lisää sen sitten otsakkeeseen X-Tenant-ID sen jälkeen, kun asiakkaalta saatu arvo on nimenomaisesti tyhjennetty: suoja header_up X-Tenant-ID "" edeltää lisäystä, joten asiakkaan lähettämä otsake ei voi päätyä demonille, tehtiinpä tiedostoon myöhemmin mitä muutoksia tahansa.

Sama pätee X-Read-Tenants-otsakkeeseen (Useiden tenanttien lukeminen): välityspalvelin poistaa asiakkaan lähettämän ja asettaa sen vain, jos se tuntee tilien välisen suhteen; repositorion esimerkit eivät tunne yhtään ja poistavat sen aina.

deploy/Caddyfile
# Demonstration reverse-proxy for `blunderdb serve` (ADR-0005).
#
# This is the WHOLE security boundary of the daemon: it authenticates the
# caller (here, HTTP Basic Auth — swap for forward_auth to a real identity
# provider, or an OIDC plugin, in production) and is the only thing allowed
# to set X-Tenant-ID. blunderdb-serve trusts that header completely and
# performs no authentication of its own.
#
# Demo credentials — CHANGE THESE before using this anywhere but a laptop:
#   alice / demo-password
#   bob   / demo-password
# Generate a real hash with:
#   docker run --rm caddy:2-alpine caddy hash-password --plaintext '<password>'
{
	# This demo terminates plain HTTP on a fixed port instead of Caddy's
	# automatic HTTPS, which needs a real public domain name to obtain a
	# certificate for. Point a domain at this host, replace ":80" below with
	# that domain, and delete these two lines to get HTTPS for free.
	auto_https off
	admin off
}

:80 {
	basic_auth {
		alice $2a$14$7yGnM3/IY8G/.mcBHMbDveecDQbnJnvHPcJQZcbWqn.H.mpttw9/.
		bob $2a$14$7yGnM3/IY8G/.mcBHMbDveecDQbnJnvHPcJQZcbWqn.H.mpttw9/.
	}

	# Map the authenticated login (Caddy sets {http.auth.user.id} once
	# basic_auth succeeds) to the tenant's positive integer — the only
	# spelling of X-Tenant-ID the daemon accepts (ADR-0005, amendment
	# 2026-09-03). This is the identity-to-tenant mapping ADR-0005 says is
	# the proxy's job: the daemon never sees "alice", only "1".
	map {http.auth.user.id} {tenant_id} {
		alice 1
		bob 2
		default 0
	}

	# Never reach the daemon's operator routes or its metrics through the
	# public proxy: /ops/ (vacuum, tenant purge) acts beyond the calling
	# tenant, /metrics needs no X-Tenant-ID and describes the whole daemon.
	@private path /ops/* /metrics
	respond @private 403

	reverse_proxy blunderdb-serve:8080 {
		# Guard, then inject: clear whatever the client sent BEFORE setting
		# the authenticated value, so a client-supplied X-Tenant-ID can never
		# reach the daemon no matter how this file is edited later — the
		# second line is the only one that can still be in effect once both
		# have run.
		header_up X-Tenant-ID ""
		header_up X-Tenant-ID {tenant_id}
		# X-Read-Tenants widens a read to other tenants (ADR-0063): only a
		# proxy that knows the relation (coach, club) may set it. This demo
		# knows none, so it drops whatever the client sent.
		header_up -X-Read-Tenants
	}
}

Kaksi muuta tiedostoa täydentää hakemistoa: deploy/nginx-tenant-proxy.conf toistaa saman kaavan nginx-otteena (proxy_set_header X-Tenant-ID "" ja sitten proxy_set_header X-Tenant-ID $tenant_id, mukanaan lohko map $remote_user $tenant_id) sille, jolla on jo nginx käytössä; deploy/README.md esittää uhkamallin ja sen, mitä ei koskaan pidä tehdä.

Caddyfile-tiedoston HTTP Basic -todennus on esimerkki, ei tuotantosuositus: se korvataan forward_auth-ohjauksella todelliseen identiteetintarjoajaan (OIDC, yrityksen SSO…), joka todentaa ja välittää sitten identiteetin samaan kohtaan tiedostoa. Vastaavuustaulukon kaksi salasanaa ja kaksi tiliä on korvattava samoin.

deploy/Caddyfile.oidc on OpenID Connect -resepti: Caddy kysyy oauth2-proxylta (forward_auth polulla /oauth2/auth), joka vastaa 202 ja antaa kirjautuneen tilin osoitteen otsakkeessa X-Auth-Request-Email, tai ohjaa tarjoajan kirjautumissivulle. map-lohko yhdistää osoitteen tenantin kokonaislukuun, ja sama header_up X-Tenant-ID "" -suojaus edeltää lisäystä. Compose-tiedostoon lisättävä oauth2-proxy-palvelu on tiedoston alussa.

Tenanttikohtaiset kiintiöt

Jaettu instanssi rajaa, mitä kukin tenant siltä vie, valitsimilla --quota-positions, --quota-analysis-seconds ja --quota-imports (ilman valitsinta mitään ei rajata). Laskenta-aika kerryttää jokaisesta tenantin pyytämästä moottorin laskennasta: gammonnet.analyzeMissing, gammonnet.sweepStale, gammonnet.compare, gammonnet.cubeMatrix, gammonnet.evaluate, rollout.position ja rollout.filter. Se lasketaan CPU-sekunteina: kulunut aika kerrottuna yhtä aikaa tehtyjen hakujen määrällä, joten kaikille ytimille jaettu laskenta maksaa yhtä paljon kuin sama työ asema kerrallaan tehtynä. Kun päivän aika on käytetty, nämä reitit vastaavat 429 ja koodilla quota_exceeded. Käynnissä oleva läpikäynti tai rollout.filter säilyttää tallentamansa ja päättyy tapahtumaan quota_exceeded tapahtuman done sijaan; keskeytetty rollout.position vastaa 429 eikä tallenna mitään; keskeytetty vertailu palauttaa kokoamansa arvolla quotaExceeded: true ja kentässä gathered niiden asemien määrän, jotka sen piti tutkia. Laskuri nollautuu UTC-keskiyöllä ja elää muistissa: daemonin uudelleenkäynnistys nollaa sen. Positiokiintiö tarkistetaan tuonnin alussa, eikä tuontia keskeytetä kesken: tenant voi ylittää sen sen verran kuin sen käynnissä olevat tuonnit lisäävät. positions.save ja muut yksittäiset kirjoitukset eivät tarkista sitä. Jokainen hylkäys sisältää kentässä details rajan (quota, limit) ja käytön (used). tenants.quota palauttaa kutsuvalle tenantille sen rajat ja käytön: tallennetut positiot, päivän laskentasekunnit, käynnissä olevat tuonnit.

Kiintiöt ovat daemonin kirjanpitoa, eivät raja: ne koskevat tenanttia, jonka välityspalvelin on asettanut otsakkeeseen X-Tenant-ID.

Täydellinen skenaario, tyhjästä vastaavaan demoniin:

git clone https://github.com/kevung/blunderDB.git
cd blunderDB/deploy
cp .env.example .env    # POSTGRES_PASSWORD
docker compose up -d --build

# 401
curl -i http://localhost:8080/v1/metadata.counts -d '{}'

# 200
curl -u alice:demo-password http://localhost:8080/v1/metadata.counts -d '{}'
curl -u alice:demo-password -H "X-Tenant-ID: 999" \
     http://localhost:8080/v1/metadata.counts -d '{}'

docker compose logs blunderdb-serve
docker compose down -v

Ensimmäisen pyynnön hylkää Caddy jo ennen kuin se saavuttaa demonin. Kaksi seuraavaa todennetaan tilinä ”alice”, jonka vastaavuustaulukko liittää tenanttiin 1: ne palauttavat saman rungon ({"positions":0,"analyses":0,"matches":0,…}) ja demonin lokissa lukee tenant=1 niistä kummallakin — asiakkaan lähettämä arvo 999 ei selvinnyt Caddyfile-tiedoston suojasta. Tämä skenaario on toistettu sellaisenaan.

Jos haluat vetää julkaistun kuvan sen sijaan, että rakentaisit sen, korvaa tiedostossa docker-compose.yml palvelun blunderdb-serve kolme build:-riviä yhdellä image:-rivillä ja aja sitten docker compose up -d ilman valitsinta --build:

blunderdb-serve:
  image: ghcr.io/kevung/blunderdb-serve:<version>
  restart: unless-stopped

Compose-tiedosto julkaisee Caddyn portin kaikissa verkkoliitännöissä (8080:80): juuri sitä välityspalvelimelta odotetaan, sillä se on olemassa tavoitettavaksi. Sitä, mitä ei saa koskaan julkaista, on demoni — eikä sitä julkaistakaan, sillä sillä ei ole lainkaan ports:-määritystä.

Käyttöönoton päivittäminen

Skeema migroidaan automaattisesti käynnistyksessä, ja tämä migraatio on yksisuuntainen: uudempaan skeemaan migroitua tietokantaa ei voi enää lukea blunderDB:n aiemmalla versiolla (katso Liite: Tietokannan skeema). Toimenpiteiden järjestyksellä on siis väliä.

  1. Varmuuskopioi ensin, ennen kaikkea muuta: se on ainoa paluu taaksepäin (katso Varmuuskopiointi ja palautus).

  2. Vedä halutun version tunniste, tuotannossa ei koskaan latest. latest seuraa viimeisintä julkaistua versiota: siihen kiinnitetty käyttöönotto vaihtaa versiota uudelleenkäynnistysten mukana, ilman että sitä olisi päätetty tai että vaiheen 1 varmuuskopio olisi välttämättä tuore.

  3. Käynnistä demoni uudelleen uudella kuvalla. Se migroi skeeman ennen kuin palvelee ainuttakaan pyyntöä; jos migraatio epäonnistuu, se pysähtyy virheeseen sen sijaan, että palvelisi puoliksi migroitua tietokantaa.

  4. Tarkista valmiussonda. GET /readyz vastaa 200 ja {"status":"ready","version":"…"}, kun tallennus vastaa ja sen skeema on binäärin skeema; 503 ja {"status":"down"}, kun tietokantaa ei tavoiteta; 503 ja {"status":"version_mismatch","version":"…","expected":"…"}, kun skeemat eroavat toisistaan — vastaus nimeää sekä tietokannan skeeman että sen, jota binääri odottaa. blunderdb healthcheck antaa saman tuomion paluukoodina.

Uudelleenkäynnistyksen jälkeen jäävä version_mismatch tarkoittaa paluuta taaksepäin: vanhempi binääri jo migroidun tietokannan edessä. Alaspäin migraatiota ei ole; palautettava on vaiheen 1 varmuuskopio.

Tärkeä

Ennen kuin otat --read-tenants käyttöön olemassa olevassa käyttöönotossa, päivitä välityspalvelin: ennen tätä otsaketta määritetty välityspalvelin poistaa vain X-Tenant-ID:n ja välittäisi sellaisenaan asiakkaan lähettämän X-Read-Tenants-otsakkeen, jolloin asiakas lukisi muita tenantteja. Ilman asetusta demoni hylkää tämän otsakkeen: otsakkeen läpäisevä välityspalvelin paljastuu 400-vastauksista.

PostgreSQL-taustajärjestelmä ja monikäyttäjäisyys

Jaettua käyttöönottoa varten blunderDB voi tallentaa tiedot PostgreSQL:ään SQLite-tiedoston sijaan. Taustajärjestelmä valitaan valinnalla --backend postgres ja yhteysmerkkijonolla --dsn. Skeema luodaan ja migroidaan automaattisesti käynnistyksen yhteydessä.

Tiedot on eristetty tenanteittain (tenant-kohtaisesti): jokainen pyyntö kantaa tenanttinsa tunnisteen (otsake X-Tenant-ID, positiivinen desimaalikokonaisluku kuten 1 tai 42), minkä ansiosta useat käyttäjät voivat jakaa saman instanssin näkemättä toistensa tietoja. Tunniste, joka ei ole tällainen kokonaisluku — nimi kuten alice tai default, 0, 007 — hylätään vastauksella 400 invalid: käänteisvälityspalvelin yhdistää tilin sen kokonaislukuun, demoni ei koskaan arvaa.

Row-Level Security

Valitsin --rls ottaa lisäksi käyttöön PostgreSQL:n Row-Level Securityn. Jokaisella käynnistyksellä demoni asentaa jokaiseen tenant_id-sarakkeen sisältävään tauluun käytännön tenant_isolation, joka päästää läpi vain istuntoparametrin current_setting('app.tenant_id') nimeämän tenantin rivit, ja pakottaa sen taulun omistajaan asti (FORCE ROW LEVEL SECURITY). Tämä parametri asetetaan yhteydelle, kun se otetaan poolista, ja nollataan, kun se palautetaan; yhteys ilman tenanttia ei näe yhtään riviä eikä lisää yhtään. Kyseessä on valinnainen syvyyspuolustus, oletuksena pois käytöstä: sovelluskoodin tenanttikohtainen suodatus on voimassa kummassakin tapauksessa.

  • Yhteysroolin on oltava tavallinen: ei pääkäyttäjä eikä BYPASSRLS. PostgreSQL päästää nämä kaksi kaikkien käytäntöjen läpi sanaakaan sanomatta, ja eristys palautuu pelkän sovelluskoodin varaan. Saman roolin on kuitenkin omistettava taulut, sillä juuri se suorittaa komennot ALTER TABLE ja CREATE POLICY.

  • Jo täytetyssä tietokannassa ei ole mitään migroitavaa: käytäntöjen asentaminen on idempotenttia DDL:ää, joka toistetaan jokaisella käynnistyksellä skeeman migraation jälkeen. Mitään dataa ei siirretä eikä yhtään riviä kirjoiteta uudelleen; valitsimen --rls käyttöönotto tai poisto on pelkkä uudelleenkäynnistys.

  • Kustannus on mitattu: yhden aseman lukemisessa 101,8 µs ilman ja 177,0 µs kanssa, eli +73,8 % — sama kontti, samat rivit, kaksi poolia, jotka eroavat vain tästä lipusta. Se maksetaan jokaisella yhteyden otolla poolista (parametrin asetus ja sen nollaus) sekä siitä predikaatista, jonka jokainen kysely ylimääräisenä läpäisee, ei koskaan datan määrästä.

Tenantin avaaminen ja sulkeminen

Palvelimen puolella ei ole mitään luotavaa: tenant ei ole tietue, vaan kokonaisluku, jota sen rivit kantavat. Tietokannassa ei ole tenanttien taulua eikä demoni pidä niistä mitään luetteloa — tilin avaaminen on merkinnän lisääminen välityspalvelimen vastaavuustaulukkoon, ja jäsenen ensimmäinen kirjoitus saa hänen tenanttinsa olemaan olemassa.

Tyhjä tenant vastaa kuin tyhjä tietokanta, ilman virhettä: metadata.counts palauttaa nollia eivätkä listat palauta mitään.

Kun tenant poistetaan käytöstä, POST /ops/tenant.purge poistaa pysyvästi kaikki sen tiedot (asemat, ottelut, kokoelmat, historian jne.) nykyiseltä tenantilta (se, jonka otsake X-Tenant-ID määrittää), sekä sen istunnon tilan (viimeisin haku, viimeisin asema, avoimet välilehdet — session_state-taulun rivit, jotka kuuluvat tälle tenantille): toiminto suoritetaan yhdessä transaktiossa, on idempotentti (tyhjän tenantin tyhjentäminen tai kutsun toistaminen ei aiheuta virhettä) eikä vaikuta muihin tenantteihin. Se poistaa tämän tenantin rivit kaikista tauluista, joissa tenant esiintyy, ja jättää jäljelle vain sen, mikä ei kuulu kenellekään: metadata-taulun, siis myös skeeman version globaalin rivin, sekä migraatiolokin. Tyhjennetystä tenantista tulee siten täsmälleen tyhjä tenant, ja sen kokonaisluku voidaan antaa uudelleen. Se on käytettävissä vain PostgreSQL-taustajärjestelmän kanssa — SQLite-taustajärjestelmässä, jolla ei ole tenantin käsitettä, se palauttaa invalid-virheen.

Tiivistys ja yhteyspooli

POST /ops/maintenance.vacuum tiivistää demonin SQLite-tiedoston — käyttöliittymän ”Tiivistä tietokanta” -painikkeen ja komennon blunderdb vacuum (katso Komentoriviliittymä (CLI)) vastine, samalla levytilan varmistuksella — ja palauttaa koot ennen ja jälkeen (sizeBefore, sizeAfter, tavuina). Se on käytettävissä vain SQLite-taustajärjestelmän kanssa; PostgreSQL:llä, jolla ei ole tiivistettävää tiedostoa, se palauttaa invalid-virheen.

PostgreSQL-yhteyspoolia säädetään ympäristömuuttujilla: BLUNDERDB_POSTGRES_MAX_CONNS (oletuksena 50), BLUNDERDB_POSTGRES_MIN_CONNS (5), BLUNDERDB_POSTGRES_MAX_CONN_LIFETIME (1h), BLUNDERDB_POSTGRES_HEALTH_CHECK_PERIOD (30s), BLUNDERDB_POSTGRES_CONNECT_TIMEOUT (5s — sen ylittyessä tavoittamaton tietokanta epäonnistuu nopeasti sen sijaan, että se jäisi jumiin käyttöjärjestelmän TCP-aikakatkaisuun) ja BLUNDERDB_POSTGRES_MAX_CONN_IDLE_TIME (30m — liikennepiikkiä varten avattu yhteys ei jää pooliin loputtomiin piikin päätyttyä). Jokainen arvo on Go-muotoinen kesto (5s, 30m, 1h); puuttuessaan tai ollessaan virheellinen se palautuu oletusarvoonsa. Kun --metrics on käytössä, poolin tila julkaistaan jatkuvasti osoitteessa /metrics: blunderdb_pg_pool_acquired (parhaillaan käytössä olevat yhteydet), _idle (vapaana olevat), _max (asetettu yläraja) ja _wait_count (niiden Acquire-kutsujen kertynyt määrä, joiden on täytynyt odottaa vapaata yhteyttä).

SQLite-tietokannan siirtäminen PostgreSQL:ään

blunderdb migrate kopioi yhden käyttäjän SQLite-tietokannan PostgreSQL-taustajärjestelmään valitun tenantin alle — kokonaisluku, jonka käänteisvälityspalvelin lähettää X-Tenant-ID-otsakkeessa tälle käyttäjälle — tämä on tapa ”ladata” työpöytäkirjasto palvelinasennukseen.

blunderdb migrate \
    --from sqlite:///path/to/database.db \
    --to   "postgres://user:pass@host:5432/db?sslmode=disable" \
    --tenant-id 42

# --dry-run
blunderdb migrate --from sqlite:///path/to/database.db \
    --tenant-id 42 --dry-run

Migraatio kopioi asemat, niiden analyysit ja kommentit, ottelut (pelit + siirrot), turnaukset (ottelulinkkeineen) ja kokoelmat (koostumuksineen), kohdistaen pää- ja viiteavaimet uudelleen, kaiken kohdepuolen yhdessä transaktiossa: toiminto on atominen (epäonnistuminen jättää kohteen koskemattomaksi, riittää että käynnistää uudelleen). Edistyminen ja lopullinen yhteenveto tulostetaan NDJSON-muodossa vakiotulosteeseen. Jos lähdetietokanta on niin vanha, että se tarvitsee oman paikallaan tehtävän skeemapäivityksen, se ajetaan ensin ja lähettää omat "schema-migration"-tapahtumansa (vaihe/tehty/yhteensä), ennen kuin rivi riviltä kopiointi alkaa.

Valinta

Oletus

Merkitys

--from <uri>

–

lähde-SQLite-tietokanta (sqlite:///<polku> tai pelkkä polku)

--to <dsn>

–

kohde-PostgreSQL:n DSN (postgres://…)

--tenant-id <n>

–

kohdetenant, positiivinen desimaalikokonaisluku (pakollinen paitsi --dry-run-tilassa; nimi kuten mon-tenant hylätään)

--dry-run

–

laskee, mitä kopioitaisiin, kirjoittamatta mitään

--on-conflict <käytäntö>

""

"" keskeyttää, jos tenantilla on jo tietoja; skip yhdistää (asemien deduplikointi Zobrist-hashin perusteella)

Muista

Vielä migroimatta jäävät sovellustilat: Anki-pakat/-kortit, suodatinkirjasto, haku- ja komentohistoria sekä istunnon metatiedot. Etusijalla on asemakirjaston ja otteluhistorian migraatio.

Yleiskäyttöinen call-välittäjä

Aiempien alikomentojen (Komentoriviliittymä (CLI)) lisäksi blunderdb call tarjoaa kaikki tallennustoiminnot suoraan paikallisesti. Se kulkee samojen käsittelijöiden kautta kuin serve-demoni: toiminta on siis identtinen komennon POST /v1/<perhe>.<metodi> kanssa. Tämä on hyödyllistä skriptauksessa ja integraatiotesteissä.

# --list
blunderdb call --list

# read
blunderdb call metadata.counts --db database.db
blunderdb call positions.list  --db database.db --json '{"limit":10}'
blunderdb call matches.get     --db database.db --json '{"id":1}'

# write
blunderdb call positions.save  --db database.db --json '{"position":{...}}'
blunderdb call matches.delete  --db database.db --json '{"id":42}'

# a gesture of a tournament Direction, with the version a read printed
blunderdb call directions.enterResult --db database.db --if-match '…' \
  --json '{"tournamentId":3,"matchId":"m7","winner":"aa"}'

Valinnat:

Valinta

Oletus

Merkitys

--db <polku>

–

SQLite-tiedosto (oikotie komennolle --backend sqlite --dsn <polku>)

--backend <tyyppi>

sqlite

sqlite tai postgres

--dsn <merkkijono>

$BLUNDERDB_DSN

taustajärjestelmän yhteysmerkkijono

--scope <n>

1

tenant, positiivinen desimaalikokonaisluku (lähetetään X-Tenant-ID-otsakkeena; nimi kuten alice hylätään)

--json <merkkijono>

{}

pyynnön runko JSON-muodossa

--json-file <polku>

–

lukee pyynnön rungon tiedostosta

--list

–

näyttää kaikki metodit <perhe>.<metodi> ja lopettaa

--if-match <version>

–

If-Match-otsakkeessa lähetetty versio, jota vaativat turnauksen johdon toiminnot (Johtotoiminnot) ja litteroinnin toiminnot (Litterointi rajapinnan kautta)

call palvelee litterointieleet ilman lippua: se toimii paikallisella tiedostolla kuten CLI. Jokainen kutsu on uusi prosessi ja siten oma istuntonsa: sessionId voidaan jättää pois, eikä kumoamista voi tehdä kutsusta toiseen.

JSON-vastaus (tai NDJSON-virta *.list-päätepisteille) kirjoitetaan vakiotulosteeseen. Virhetilanteessa prosessi päättyy nollasta poikkeavalla koodilla ja kuori {"error":{…}} tulostetaan vakiotulosteeseen, jotta se pysyy jäsenneltävänä (esimerkiksi jq:lla). Vastaus, jossa on otsake Direction-Version, tulostaa sen virhetulosteeseen: se on arvo, jonka seuraava ele antaa valitsimelle --if-match. call palvelee turnauksen johtamisen eleitä ilman valitsinta, kuten CLI, koska se suoritetaan paikallisesti.

Työkalut tekoälyavustajalle (MCP)

blunderDB ei sisällä kielimallia: se tarjoaa työkalunsa jo käyttämällesi avustajalle (Claude Code, Claude Desktop, paikallinen asiakas) Model Context Protocol -protokollan kautta. Avustaja hakee, lukee ja selittää; blunderDB vastaa omilla luvuillaan.

Työkalut kulkevat samojen käsittelijöiden kautta kuin /v1 ja call:

Työkalu

Mitä se palauttaa

database_overview

lukumäärät, otteluiden ajanjakso, skeemaversio, usein esiintyvät pelaajat

search_positions

komentorivin kieliopilla tehdyn haun asemat (kuvattu työkalussa) kanonisessa muodossaan

search_comments, saved_searches

tiettyjä sanoja sisältävät kommentit; tallennetut haut

get_position

asema, sen analyysi (parhaat siirrot tai tuplaus), pelattu siirto ja kommentti

explain_error

virheen teema, sen hinta millipisteinä ja paras ratkaisu

similar_positions, decode_position, legal_moves, race_epc

lähellä olevat asemat; XGID:n lukeminen; sallitut siirrot; kisan EPC

list_players, player_stats, recurring_errors, training_stats

pelaajat; kokonais-PR, nappulat, tuplauskuutio, vaiheittain; toistuvat virheet; visan PR ja Ankin pysyvyys todellista PR:ää vasten

list_matches, get_match, list_tournaments

ottelut, yksittäisen ottelun tiedot, turnaukset

list_collections, collection_positions, study_decks

kokoelmat ja niiden asemat; opiskelupakat

quiz_draw, quiz_grade

arpoo aseman ilman vastausta ja arvioi sitten annetun vastauksen

evaluate

tekstinä annetun aseman gammonNet-arvio tallentamatta sitä: parhaat siirrot tai tuplauspäätös

anki_next

tarkistuspakan seuraava erääntynyt kortti

transcribe_list, transcribe_get, transcribe_mat

ottelujen litteroinnit; yhden litteroinnin tiedot; sen .mat-teksti

direction_list, direction_standings, direction_season

johdetut turnaukset; turnauksen sijoitukset; kauden sijoitus

rollout

kirjaston aseman rollout: ekvity, 95 %:n väli ja JSD ehdokasta kohti

Vain viisi työkalua kirjoittaa — save_position, comment_position, create_collection, add_to_collection ja anki_review, joka arvioi anki_next-työkalun arpoman kortin — ja ne tarjotaan vain pyynnöstä: --write paikallisesti, --mcp-write demonissa. Kaikki muut vain lukevat; rollout saa kuitenkin, kun kirjoitus on tarjolla, argumentin store, joka tallentaa rolloutin aseman analyysin viereen. Mikään työkalu ei poista mitään.

Paikallisesti avustaja käynnistää komennon blunderdb mcp tiedostolle (ks. Komentoriviliittymä (CLI)). Claude Codelle:

claude mcp add blunderdb -- blunderdb mcp --db /chemin/vers/base.db

Demonilla samat työkalut vastaavat HTTP:llä osoitteessa POST /mcp (streamable HTTP -siirto, ilman istuntoa). Kuten /v1, /mcp vaatii X-Tenant-ID:n ja jokainen työkalu toimii kyseisessä tenantissa; myös ohjelma, joka upottaa pkg/blunderdb/server:n, tarjoaa sen. Demoni ei tunnista ketään (ADR-0005): /mcp suojataan välityspalvelimella kuten /v1, ja --mcp-write päätetään siellä kuten --direction. Jokainen työkalun tekemä /v1-kutsu kulkee uudelleen palveludaemonin koko ketjun läpi: se kirjataan lokiin, lasketaan mittareihin ja veloitetaan vuokralaisen nopeusrajasta, sen /mcp-pyynnön lisäksi, joka sen kuljettaa. Työkalukutsu maksaa siis useita pyyntöjä; mitään ei vapauteta.

Kuten call, blunderdb mcp siirtää vanhemman tietokannan skeeman avattaessa, myös ilman --write-valitsinta.