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 |
|---|---|---|
|
– |
SQLite-tiedosto (oikotie komennolle |
|
|
tallennustaustajärjestelmä: |
|
|
taustajärjestelmän yhteysmerkkijono |
|
|
kuunteluosoite |
|
|
lokitustaso: |
|
|
tarjoaa |
|
|
tarjoilee selauskäyttöisen verkkosivun osoitteessa |
|
|
palvelee turnauksen ja tapahtuman johtotoiminnot; oletuksena pois päältä, ks. Johtotoiminnot |
|
|
tarjoaa |
|
|
palvelee litterointieleet ( |
|
|
sulkee litterointi-istunnon, joka on ollut käyttämättä tätä kauemmin |
|
– |
ottaa CORSin käyttöön tälle alkuperälle, pilkuin erotellulle alkuperäluettelolle tai arvolle |
|
|
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 |
|
|
token-ämpärin koko pyyntöpiikkejä varten |
|
|
positioiden määrä, jonka tenant voi tallentaa, tarkistetaan tuonnin alussa: kun raja on saavutettu, tuonti hylätään (413, |
|
|
moottorin laskenta-aika CPU-sekunteina tenanttia ja UTC-vuorokautta kohden (429, |
|
|
saman tenantin samanaikaisesti käynnissä olevat tuonnit (429, |
|
|
PostgreSQL: ottaa käyttöön tenant-kohtaisen Row-Level Securityn (syvyyspuolustus, valinnainen) |
|
|
noudattaa |
|
– |
valinnainen kaksipuolinen bearoff-tietokanta ( |
|
– |
demonin liikkeeseenlaskijaidentiteetin hakemisto (luodaan ensimmäisellä käyttökerralla); tarvitaan, jotta |
|
– |
tarjoaa |
|
– |
julkaisee |
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--metricson käytössä);GET /app/— selauskäyttöinen verkkosivu (jos--webon 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.listjadirections.directorylukevat 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 suodattimetplayerjamatch),directions.clock,directions.slots,directions.lastDecision,directions.pageHtmljadirections.pairingSheetHtml(parametrillaround).rencontres.list, sittenrencontres.getjarencontres.pageHtmlparametrilla{"id": N}.rencontres.pageHtmltuottaa 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.rankingpalauttaa kauden sijoituksen, kutenblunderdb tournament ranking --season:rencontreId,from,to,points,participationjaelo, kaikki valinnaisia; ilmanrencontreId: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.setConfigjadirections.previewConfig(config, moottorin JSON-muotoinen asetus);ilmoittautumiset:
directions.enterParticipants(players),directions.addParticipant(name,club,rating; arvoillasectionjakeymyöhästyjä saa vapaakierrospaikan),directions.updateParticipant,directions.withdraw,directions.reinstate,directions.makeAbsent,directions.makeAvailable,directions.addPair,directions.updatePair;kulku:
directions.confirmProposal(action, sellaisena kuindirections.getsen 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ä) jadirections.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. Virheendetails-kenttä sisältää tuoreen tilan ja senversion: 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
428tai409, 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) javersion;event: direction—tournamentIdjaversion, tapahtuman ulkopuolella pelattavalle turnaukselle;event: transcription—transcriptionIdjarevision; hylätyllä tai päätetyllä luonnoksella onremoved(jamatchIdPää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 onmissed. PostgreSQL:lle liian pitkä ilmoitus (8 000 tavua) tai ilmoitus, jota instanssi ei ole voinut lähettää, saapuu muille samanaresync-viestinä kyseiselle tenantille.Virran
id-arvot ovat kunkin instanssin omia. Asiakas, jonka kuormantasaaja ohjaa toiseen instanssiin, ei hyödy niistä: jokaisen virran avaavaresyncsaa 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 taipg_database_sizePostgreSQL: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:
palvelimelta työasemalle —
POST /v1/exports.sqlitekirjoittaa koko nykyisen tenantin.db-tiedostoon, jonka työpöytäsovellus avaa sellaisenaan (katso Varmuuskopiointi ja palautus);työasemalta palvelimelle —
blunderdb migratekopioi.db-tiedoston halutun tenantin alle (katso SQLite-tietokannan siirtäminen PostgreSQL:ään).
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.
# 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.
# 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ä.
Varmuuskopioi ensin, ennen kaikkea muuta: se on ainoa paluu taaksepäin (katso Varmuuskopiointi ja palautus).
Vedä halutun version tunniste, tuotannossa ei koskaan
latest.latestseuraa 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.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.
Tarkista valmiussonda.
GET /readyzvastaa200ja{"status":"ready","version":"…"}, kun tallennus vastaa ja sen skeema on binäärin skeema;503ja{"status":"down"}, kun tietokantaa ei tavoiteta;503ja{"status":"version_mismatch","version":"…","expected":"…"}, kun skeemat eroavat toisistaan — vastaus nimeää sekä tietokannan skeeman että sen, jota binääri odottaa.blunderdb healthcheckantaa 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 komennotALTER TABLEjaCREATE 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
--rlskä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 |
|---|---|---|
|
– |
lähde-SQLite-tietokanta ( |
|
– |
kohde-PostgreSQL:n DSN ( |
|
– |
kohdetenant, positiivinen desimaalikokonaisluku (pakollinen paitsi |
|
– |
laskee, mitä kopioitaisiin, kirjoittamatta mitään |
|
|
|
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 |
|---|---|---|
|
– |
SQLite-tiedosto (oikotie komennolle |
|
|
|
|
|
taustajärjestelmän yhteysmerkkijono |
|
|
tenant, positiivinen desimaalikokonaisluku (lähetetään |
|
|
pyynnön runko JSON-muodossa |
|
– |
lukee pyynnön rungon tiedostosta |
|
– |
näyttää kaikki metodit |
|
– |
|
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 |
|---|---|
|
lukumäärät, otteluiden ajanjakso, skeemaversio, usein esiintyvät pelaajat |
|
komentorivin kieliopilla tehdyn haun asemat (kuvattu työkalussa) kanonisessa muodossaan |
|
tiettyjä sanoja sisältävät kommentit; tallennetut haut |
|
asema, sen analyysi (parhaat siirrot tai tuplaus), pelattu siirto ja kommentti |
|
virheen teema, sen hinta millipisteinä ja paras ratkaisu |
|
lähellä olevat asemat; XGID:n lukeminen; sallitut siirrot; kisan EPC |
|
pelaajat; kokonais-PR, nappulat, tuplauskuutio, vaiheittain; toistuvat virheet; visan PR ja Ankin pysyvyys todellista PR:ää vasten |
|
ottelut, yksittäisen ottelun tiedot, turnaukset |
|
kokoelmat ja niiden asemat; opiskelupakat |
|
arpoo aseman ilman vastausta ja arvioi sitten annetun vastauksen |
|
tekstinä annetun aseman gammonNet-arvio tallentamatta sitä: parhaat siirrot tai tuplauspäätös |
|
tarkistuspakan seuraava erääntynyt kortti |
|
ottelujen litteroinnit; yhden litteroinnin tiedot; sen |
|
johdetut turnaukset; turnauksen sijoitukset; kauden sijoitus |
|
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.