Képzeljünk el egy bankot a fiókjaival földrajzilag osztott elhelyezkedésben. Annak érdekében, hogy egy egyedi fiók számláira vonatkozó információkat lokálisan el lehessen érni, a számlák online adatbázisa is fizikailag osztott, minden egyes fiók számláira vonatkozó információ az adott fiók számítógépében vagy számítógépeiben van tárolva. Mindemellett a rendszer egyik fontos célja, hogy támogassa a számlákkal végzett távoli beavatkozásokat. Például egy ügyfél, akinek számlája egy bizonyos fióknál van, végrehajthat betétet vagy kivétet egy másik fióknál, vagy akár pénzváltó automatánál. Ezenkívül, a bank központjában dolgozó alkalmazottak is elérhetik bármely fiók adatait pl. összegzésre vagy egyéb célból.
A banki rendszer konfigurációja a következő. Minden hivatalnok kapcsolatban áll egy miniszámítógépen futó front-end programmal, egyetlen mini futtathatja a front-end programokat sok hivatalnok számára. A pénzváltó automaták a miniszámítógép egy másik programjához kapcsolódnak. A rendszer más felhasználói is futtathatják a programjukat a miniszámítógépen. Például egy banki alkalmazott elkészítheti a havi zárást vagy összegezheti a tranzakciókat egy ilyen programmal. A miniszámítógépek egy hálózaton keresztül kapcsolódnak a back-end számítógépekhez, ahol a számla-információk vannak tárolva. Minden mini kommunikálhat minden back-end számítógéphez és fordítva.
Minden back-end a bank egy fiókjához tartozik, és tartalmazza a fiók adatbázisát a helyi számlák adatairól. Egy számláról a következő információkat tároljuk: a számla tulajdonosának neve és címe, a számla egyenlege és egyéb információk, pl. a számlára vonatkozó tranzakciók feljegyzését.
A rendszer használatára egy példa a következő eset. Adott egy összegző eljárás, amely lehetőséget ad az adminisztrátornak, hogy kiszámítsa néhány fiók összes aktivumát, az érdekelt fiókok azonosítója egy vektorban van adva. Az audit küld egy igényt minden érdekelt fióknak az aktuális egyenlegre vonatkozóan, majd a az eredményeket összegzi. Ez a program objektum orientált abban az értelemben, hogy a fiók-adatbázisok objektumok, amelyeket lehet kérni igények végrehajtására, pl. betét, kivét. A b.total a b-nek küldött igény az egyenlegre vonatkozóan. A betét, kivét ill. az egyenleg kiszámítása magánál a fióknál hajtódik végre lokálisan.
A transfer eljárást egy hivatalnok használja, abból a célból, hogy pénzt utaljon át a from számláról a to számlára. Az eljárás vagy normálisan terminál, ami azt jelenti, hogy az átutalás sikeres volt, vagy jelzi az insufficient_funds kivételt, ha a from számla egyenlege kisebb, mint az átutalni kívánt összeg. Az átutalás eljárás a from számláról való kivételt, illetve a to számlára való betételt hajtja végre. Először végrehajt egy kivét kísérletet a from számláról, ez az igény a from fiókjához irányul. Ha van a from számlán elegendő készpénz, akkor a kívánt összeget beteszi a to számlára, amely a to fiókjához irányuló betét igényt jelent. A get_branch függvény számlaszám alapján megmondja, hogy a számla melyik fiókhoz tartozik.
Ezek az eljárások számos problémát vetnek fel, pl.:
Egy osztott program számos különböző csomópontokban elhelyezkedő guardian-ból áll össze. Pl. a banki rendszerben lehet egy guardian futtatva minden fiók back-end számítógépén, amely feldolgozza a fiók számláira érkező igényeket. Lehet egy input guardian, amely egy háttér eljárást használva figyeli az inputot, majd meghívja a megfelelő fiók-guardian-t. A fiók guardian-ok a számlákkal kapcsolatos összes fontos információt tárolják egy állandó objektumban, így egy abortálás esetén is vissza lehet állítani az eredeti állapotot.
Az ARGUS a CLU nyelv kiterjesztése, a szintaktika és szemantika nagy részét a CLU-ból veszi. A guardian definíciója egy header-rel kezdődik, amely leírja a guardian típusát, ill. a handler-ek neveit. A fiók guardian-ban kétféle típusú művelet van. A creator-ok amelyek a tipus egy új guardian-ját hozzák létre, míg a handler-ek a műveletek, amelyeket a guardian nyújt, mihelyt létrehozták. Néhány típusú guardian-hoz hasznos, ha több creator-t is megadunk, de a fiók egyetlen creator-a a create. Az öt handler: open, close, deposit, withdraw, total.
A guardian-on belül először a típus-definíciók és a változók deklarációi következnek, amelyek a guardian állapotát határozzák meg. A mi esetünkben a guardian teljes állapota állandó, mivel az összes állapotot meghatározó változó stable-nak van deklarálva. Az állapot három objektumot tartalmaz: hash table, amely biztosítja a számlákhoz való hozzáférést, a fiók egyedi kódja,és a seed, amelyet egy új számla számára való egyedi név generálására használunk.
Mivel a deposit-ot és a withdrawal-t valószínűleg igen gyakran használjuk, ezért azt akarjuk, hogy ezek gyorsak legyenek. Néhány megfontolandó dolog: a számla helyének megállapításának gyorsnak kell lennie, az egyidejű kivét és betét nagyon valószínű, ezért mi ezt meg akarjuk engedni, ha csak lehetséges, végül minimalizálni akarjuk az állandó memóriába való írásnak a költségét, ami szükséges ahhoz, hogy a különböző operációk eredményét tároljuk.
A fiók guardian reprezentációja megfelel ezeknek a céloknak. A számlát megkeresésére a hash_table-t használjuk, amely leképzi az egész számokat a bucket-ekre. Egy bucket az egy atomi vektor, amelynek minden eleme tartalmaz információt egy számláról. A tárolt információ a számla száma és az objektum, amely magára a számlára vonatkozó információt tartalmazza. Egy account number az egy atomi rekord két komponenssel, az első komponens az ő fiókjának a kódja, a második, amely a fiókon belül azonosítja őt. Az egyetlen információ, amelyet a számláról tárolunk az egyenleg, egy valódi implementációban természetesen több információt kell tárolnunk. A seed egy atomi rekord-ban van tárolva, amely egyetlen integer komponenset tartalmaz, amely megoldja a megfelelő szinkronizációt az egyidejű open-ek között. Meg kell jegyezni, hogy minden adatstruktúra atomi objektum, amely a fiók guardiant használó action-ok megfelelően szinkronizálva lesznek és hogy abort esetén nem lesz hatásuk. A read és a write lock az atomi objektumokon többnyire automatikusan szerződik meg, mikor a műveleteket meghívják, vagyis az utolsó open utasítás tartja a ht read lock-ját és a bucket write lock-ját, amíg módosítja azt az új számla hozzáadásával.
Mivel nincs volitary állapota a guardián-nak, ezért nem szükséges olyan eljárás, amely crash esetén visszaállítja az állapotot. Ráadásul a guardian-nak nincs háttér művelete, az összes tevékenység a handler hívások végrehajtásának része. A guardian összes többi része a creator és a handler-ek leírása, valamint egy külső eljárás, a look_up.
A create argumentumai a fiók kódja és a hash_table mérete. Inicializálja a guardian állapotát, majd önmagát adja vissza, vagyis az újonnan létrehozott guardian-t. A hash_table úgy van inicializálva, hogy tartalmazza a teljes üres bucket-eket. A bucket-ek lehetnek üresnek inicializálva, mivel a vektorok az ARGUS-ban nőhetnek és csökkenhetnek dinamikusan.
A total kiszámítja a fiókban lévő számlák egyenlegének összegét, egy iterátor használatával. Az iterátor egy spec. operáció, amely ahelyett, hogy visszatérne, meghívja önmagát. Amikor az iterátor elindul, az értéke hozzárendelődik a ciklusváltozóhoz, és a törzs elindul. Amikor a törzs lefutott a ciklusváltozó felveszi a következő értéket, ha már nincs több érték, akkor mind a törzs, mind az iterátor terminál. A használt iterátor előállítja a vektor elemeit first-től last-ig. A külső for utasítás iterátora előállítja a hash table minden bucket-jét. A bucket-ben lévő minden számla elérhető a külső for utasításban.
Amikor a total terminál, visszatér commit-tal és a kiszámított összeggel. Mint említettük a handler mint a hívó action subaction-ja fut. Ha a handler terminál, ez lehet abort vagy commit. A default a commit, explicit abort hiányában a handler action commit-tal fog visszatérni. Explicit abort kiváltható return(abort), signal(abort) -tal. Példánkban minden handler commit-tal tér vissza, képzeletünkben ez messze a legáltalánosabb eset. A total megszerzi a read lock-ot a hash_table-n és minden bucket-on is amíg az iterátor olvassa a hash_table-t és a bucket- eket. Megszerzi a read lock-ot minden számlánál, amíg olvassa a számlán lévő összeget. Ezek a lock-ok az ősnek adódnak tovább commit esetén.
Az open generál egy új account number-t az új számlának és megnöveli a seed-et. Először az open megszerzi a seed write lock-ját, hogy megelőzze a konkurens open-ek közötti holtpontot. (holtpont léphet fel, ha két open megkapja a read lock-ot de egyik sem képes megszerezni a write lock-ot, amely szükséges ahhoz, hogy növelje a seed- et) Az open a hash eljárást használja, hogy kiszámítsa az új számla bucket-jét. A hash paramétere az account number-nek az integer része és visszaad egy egész számot 0 és az aktuális hash_table méret között. Az open használja az addh operátort, amely megnöveli a vektort egy elemmel, és a paramétert beteszi erre az új helyre.
A close átnézi a számlákat, használva a vektornak az indexes iterátorát, hogy megkeresse a számla bucket-jét, ez az iterátor visszaadja a vektor összes legális indexét. A többi handler használja a külső look up eljárást, hogy a kérdéses számla helyét megállapítsa, vagy visszatér no_match értékkel, ha nincs ilyen számla.
Az implementáció megenged számos konkurens működést. Pl. egyidejű betét ill. kivét, ha külső számlákról van szó. Az egyidejűség megengedett, mivel ezek a műveletek csak a read lock-ot szerzik meg a hash_table-nél és a számlák bucket-jénél. A write lock csak annál az információnál van megszerezve, amely magáról a számláról tárol információkat.
A close futhat párhuzamosan az open, close, deposit, withdrawal műveletekkel, feltéve, hogy a másik hívások más számlákra vonatkoznak. Ez megakadályozza azokat a hívásokat, amelyek ugyanazt a bucket-et használják, mivel megszerzi a write lock-ot a bucket-nél (amikor hozzárendeli az i. eleméhez a bucket-nek). Open hasonló a close-hoz, kivéve, hogy ez a más konkurens open-eket is kirekeszti, mivel minden open megszerzi a write lock-ot a seed-nél. Ez a kirekesztés nem probléma ha, a bucket-ek kicsik és ha a számlák jól vannak megnyitva. Ha ez probléma, akkor a guardian állapotát lehet másképp implementálni.
A stable storage-ba való írós minden esetben kicsi, kivét, betét-nél csak a számlát magát írjuk. Open, close-nál a nyitott v. csukott számla bucket-jét kell írni. A bucket- ben lévő számlákat nem kell írni, kivéve, az újonnan nyitott számlát. Total-nál semmit sem kell írni.
A total implementációja jelenti a guardian-nál a fő problémát. A total lassú, mivel az összes számla egyenlegét számítja ki.
A total minden műveletekkel konfliktusba kerül, amíg a total hívó topaction be nem fejeződik, egyéb operációk a fióknál várakoznak. A kivét és a betét konfliktusa szükséges, ha a total-tól elvárjuk, hogy aktuális legyen.
Az adott implementációban a legtöbb handler holtpontba kerülhet egy másik konkurens művelettel. Pl. kivét, betét egy számlánál. Ennek az oka, hogy a műveletek először a read lockot szerzik meg a számlánál, és csak azután a write lockot. Ez a probléma megoldható, ha a write ill. a read lock megszerzésének sorrendjét felcseréljük, mint az open esetében tettük.
Nincs olyan művelet, amely a hash table-t megváltoztatja. Ez abból a szempontból fontos, mivel egy olyan művelet, amely módosítja a hash_table-t az összes többi művelettel konfliktusba kerül. Mivel a hash_table nagy, ezért mi nem akarjuk a stable storage-ba másolni. Természetesen szükség lehet arra, hogy a guardian-t újraszervezzük megváltoztatva annak méretét, vagy más indexelési módszert alkalmazva. A cserét meg lehet oldani egy topaction futtatásával, és a másolat az új hash table- ról és a bucket-ekről akkor kerül a stable storage-ba, amikor a topaction commit-tal visszatér.
A front-end guardiannak nincs handler-e, minden működése a háttérben zajlik. Amikor létrejön paraméterként átvesz minden információt az eszközökről, amelyet felügyel, és a többi guardian azonosítóját. A device inf.-k és a guardian inf.-k a stable storage-ben vannak tárolva. Az elérési procedure felgyorsítása érdekében fenntart egy bt nevű táblát, amelybe a külső guardian-ok információinak másolatai vannak. Ez a tábla létrehozáskor ill. crash után kerül felépítésre.
Egy átutalás a from, to számlákkal ill. az összeggel van meghatározva. Átutaláskor egy belső topaction jön létre, hogy végrehajtsa a tényleges átutalást. A get_action eljárás szolgál a két számla fiókjának meghatározására, amely megkeresi a számlák kódját a bt táblában. A két fiók ezután hívása párhuzamosan történik, egy-egy subaction-nal. Ha mindkét subaction commit-tal tér vissza, akkor a coenter befejeződik, és a topaction is commit-ot ad. Ha valamelyik hívás kivételt jelez, akkor ha a másik még fut, akkor az megszakítódik és a topaction abortál, megsemmisítve ezzel az esetleg már commit-tal befejeződött másik subaction hatását is.
Ahhoz, hogy az összegzést végrehajtsa a háttér eljárás kapcsolatba lép az összes érdekelt fiókkal, és futtatja a saját összegzőjét, mint topaction-t. Ez párhuzamosan kommunikál az összes fiókkal, használva elkülönített eljárásokat és subaction-okat a hívásra. A total atomi rekordként van fenntartva, annak érdekében, hogy az ágaknál az elérések helyesen legyenek szinkronizálva. Minden ág először megszerzi a write lock-ot annak érdekében, hogy a holtpontot elkerüljük. Ha minden hívás normálisan tért vissza, akkor a topaction commit-tal tér vissza.
Annak ellenére, hogy a total nem jelez hibát, mégis lehetséges, hogy egy hívás sikertelen, pl. ha lehetetlen pillanatnyilag kommunikálni a guardian handlerével. Ebben az esetben a hívás automatikusan unavailable üzenettel terminál. Ilyen kivétel elfordulásakor a coenter azonnal terminál, megszakítja a be nem fejezett ágakat és a topaction abortál.
Fontos, hogy az action-ok rövid ideig tartsák a write-lock-ot, mert ezalatt zavarhatnak más action-öket. Ebből az okból a kommunikáció a felhasználóval mindkét esetben action-ön kívül van megoldva. Crash előtt a front- end tájékoztatja a felhasználót az igény végrehajtásának kimeneteléről, lekerülve azt, hogy a felhasználó bizonytalan maradjon, hogy az igény végrehajtódott-e. Minden external action-nél, vagyis azon action-öknél, amelyek kapcsolatba kerülnek a külső környezettel, ez a probléma felmerül. A problémát nem lehet megoldani azzal, hogy az kommunikációt az action-ön belülre mozgatjuk, mivel nem biztos, hogy commit-tál az action, miután a user-nek azt mondta, hogy az átutalás befejeződött.
A bemutatott rendszer statikus, nincs lehetőség új fiókot hozzáadni a rendszerhez. A registry guardian-t felhasználva lehet támogatni, a rendszer dinamikus újrakonfigurálását. A front-end végrehajthat párbeszédet a felhasználóval aztán kapcsolatba léphet a registry-vel, hogy beléptesse az új információt. Pl. a felhasználó definiálhat egy kódot a fióknak, vagy megteheti ezt a registry. A felhasználónak jeleznie kell, hol helyezkedjen el az új fiók- guardian. Az ARGUS ennek érdekében biztosít egy node nevű beépített típust. A registry így létrehozhat egy új guardian-t a köv. utasítással: b: branch :=branch$create(c) @ n
Ez létre fog hozni egy új guardiant az n csomópontban és aztán futtatja az ő creator-át, a c paraméterrel. Az új guardian visszaadódik, amelyet aztán a registry table-ben lehet tárolni.
Ilyen dinamikus rendszer esetén a front-end-nek fel kell készülnie olyan információra, amely nincs benne a bt táblájában. Pl. amikor a get_branch eljárás megkeres egy account_number-t felfedezhet egy eddig ismeretlen kódot. Ebben az esetben olvasna a registry táblájából és jra próbálkozna.
Implementációnkat úgy terveztük, hogy elkerüljük a felhasználói munka felesleges várakoztatását. Két fő formája van a várakoztatásnak, amit el akartunk kerülni. A felesleges kommunikáció és a stable_storage-ba való írás. Általában a kommunikációs költség csökkenthető az információk csomagban való küldésével, ahol az információátvitelt háttérben kell végezni. Amikor több guardian szerepel egy action-ben, a várakozás minimalizálható, ha a stable_storage-ba való írást egy időben végezzük minden guardian-nál. Ráadásul a legtöbb ilyen írást háttérben is el lehet végezni. Egy pont, ahol nem lehet elkerülni a várakoztatást, amikor a topaction commit-tál, ebben az esetben szükséges az összes leszármazottal kommunikálni, és a stable_storage-ba írni.
Top vagy subaction létrehozása lokálisan megoldható abban a csomópontban, ahol az fut. Ebben az esetben csak egy új azonosítót kell kreálni és inicializálni a néhány összefüggő adatstruktúrát. Pl. a commit-tal visszatérő leszármazottakat tárolhatjuk egy plist-ben, amelyet a két fázisú commit-ban fogunk használni. Kezdetben ez a lista egyetlen elemet tartalmaz, a létrehozó guardian-t.
Amikor egy handler subaction commit-tál, emlékszik azokra a lokális atomi objektumokra, melyeknek a lock-ját megszerezte. A válaszüzenet, amely jelzi, hogy az action commit-tált, tartalmaz egyéb információt is, pl. a subaction plist-jét, amely hozzáadódik a szülő plist-jéhez.
Amikor egy subaction commit-tál azok a lock-ok és verziók, amelyek az ő guardian-jához tartoznak, továbbadódnak a szülő felé, de más guardian-hoz tartozók nem, mivel ez kommunikációt igényelne. Hasonlóan egy abortáló top vagy subaction csak a lokális verziókat és lock-okat törli. Ráadásul ebben az esetben az abort üzenetet háttér-módban küldjük a megfelelő guardian felé. Nem garantáljuk, hogy az ilyen üzenetek meg fognak érkezni, bár nagy valószínűséggel megérkeznek.
Mivel az abort üzenet lehet, hogy nem érkezik meg, és commit-ról nem is küldtünk üzenetet, a guardian-ban, ahol a locked objektum található, nem az aktuális információ lesz. Pl. egy objektum, amely locked-nek van jelölve, lehet, hogy már unlocked. Annak érdekében, hogy megállapítsuk az objektum igazi állapotát, kérdés üzenetet küldhetünk közvetlenül annak a guardian-nak, ahol a lock-ot tartó action őse futott. Ezeket a kérdéseket akkor küldjük, amikor szükségünk van a lock-ra.
Amikor a topaction commit-tál a rendszer végrehajt egy kétfázisú commit-protokollt. A protokollban a résztvevők a plist-ben lévő guardian-ok, a koordinátor a topaction guardian-ja. Az első fázisban a koordinátor küld egy elökészítő üzenetet minden résztvevőnek, akik rögzítik a verzióikat prepare rekord formájában a stable storage-ban, és ok üzenetet küldenek vissza, és törlik a read lock-okat. Sikertelenség esetén refuse üzenetet küldenek vissza.
Ha minden résztvevő ok üzenetet küldött, akkor a koordinátor beírja a stable storage-ba a commit rekordot-ot, majd commit üzenetet küld a résztvevőknek. A további működés háttérmódban zajlik. Amikor a résztvevők megkapják az üzenetet rögzítik a commit-ot a stable-storage-ban, aktualizálják a verzióikat, törlik a lock-okat és jelzik ezt a koordinátornak. A koordinátor mindaddig újra küldi ezeket az üzeneteket, amíg mindenkitől választ nem kap. A commit rekordban az egész plist tárolva van, ami garantálja, hogy a koordinátor crash-e esetén is folytatódik az eljárás.
Ha egy résztvevő refuse üzenettel tér vissza, akkor a koordinátor abortál, és ilyen üzenetet küld minden résztvevőnek. Az abort üzenet megérkezése nem garantált.