Thread-safe instance accessor, instance reinicializációval
2021-11-29T17:00:51+01:00
2021-11-29T21:31:03+01:00
2022-08-12T05:55:29+02:00
H.Lilla
Sziasztok!

Van egy olyan problémám, hogy egy szerver RESTful API-jához kaptam egy kliens kódot. Ahhoz, hogy a kliens hozzáférjen a szerverhez, authentikációra van szükség, ami X ideig érvényes tokent jelent. Maga a szerver többféle auth módszert támogat, köztük AWS IAM, Kubernetes, username/password, és sok mást is, de nekem az IAM a lényeg.

Maga a kliens elég egyszerű, de fapados is, tehát nem figyel arra, hogy az auth során kapott token lejár-e, helyette ezt a problémát a user code-ra bízza. De lényegében a kliens használata nagyjából így néz ki:

IAuthentication auth = new AwsIamAuthentication(...); IClient client = new Client(auth);
A kliens az első request küldésekor authentikál, és egyébként ad két metódust:

DateTimeOffset IClient.GetTokenExpirationTime(); //megadja, meddig érvényes a token void IClient.ResetToken(); //törli a tokent, így a következő requestnél újra hitelesít
A ResetToken jól működik olyan esetekben, mint pl. a username/password hitelesítés, de bizonyos auth módszereknél a kliens ismételt példányosítása szükséges. Erre egy példa az IAM hitelesítés, ami az AWS által sign-olt adatokra támaszkodik, de ez a sign szintén lejár Y idő után, így a teljes IAuthentication újrapéldányosítása is szükséges a kliens ismételt példányosítása mellett.

Az, hogy a ResetToken elég-e vagy kell az újrapéldányosítás, azt a user code mondja meg, mivel azt is ő mondja meg, hogy milyen auth módszert használ. Szóval legyen rá egy bool értékünk valahol:

bool recreateClientInsteadResettingToken;

A kliens kódjáról az a mondás, hogy elvileg thread-safe, és ezt meg is kellene őriznem, viszont szükségem lenne egy olyan eszközre, ami figyel arra, hogy a user code mindig olyan IClient példányt kapjon, aminek a tokenje érvényes - szintén thread-safe módon, figyelembe véve az előbb említett recreate...Token flaget.

Íme az én naiv ötletem, de elég erős megérzésem van, hogy ez így nagyon nem jó megoldás, bár nem tudom elmagyarázni, hogy miért.

class ClientOptions { public Func<IAuthentication> AuthFactoryMethod { get; } public bool RecreateClientInsteadResettingToken { get; } } class TokenRefreshingClient : IClient { private readonly ClientOptions options; private readonly object lockObject = new(); private IClient client; private DateTimeOffset tokenExpirationTime; public TokenRefreshingClient(ClientOptions options) { this.options = options; } private void GetClient() { if (client == null || tokenExpirationTime < DateTimeOffset.UtcNow) { lock(lockObject) { if (client == null || tokenExpirationTime < DateTimeOffset.UtcNow) { RecreateClientOrResetToken(); } } } return this.client; } private void RecreateClientOrResetToken() { if (client == null || options.RecreateClientInsteadResettingToken) { client = new Client(options.AuthFactoryMethod()); tokenExpirationTime = client.GetTokenExpirationTime(); } else { client.ResetToken(); } } //interface members public DateTimeOffset GetTokenExpirationTime() { return tokenExpirationTime; } public void ResetToken() { //useless in this implementation } //other interface members are implemented using GetClient() public void DoSomething() { GetClient().DoSomething(); } }
Mi ennek a kezelésére a bevált módszer? Hogyan lehet ezt jól csinálni?
Mutasd a teljes hozzászólást!
Amit a GetClient-ben csinálsz, azt double-checked lockingnak hívják. Ez önmagában elvileg működne. Az egyik gond az implementációddal, hogy egy "másik" szál a tokenExpirationTime-ot láthatja nullnak úgy, hogy a client nem null, így ennek exception lehet a vége. Amit csinálnod kéne, hogy a client-et és a tokenExpirationTime-ot becsomagolod egy immutable objektumba, és erre az objektumra tárolsz referenciát, ezen az objektumon keresztül éred el a client-et, és a tokenExpirationTime-ot is. Mivel a kódrészletből nem derül ki (és egyébként is úgy biztonságos), a becsomagoló objektumról érdemes egy local copyt csinálni az első if előtt, és a lock-béli if előtt is. Tehát nem this.wrapper.tokenExpirationTime-ot ellenőrzöl, hanem localCopy.tokenExpirationTime-ot mindkét if-ben. Illetve a return is localCopy.client-et adjon vissza. A local copy kihagyása nélkül funky dolgok történhetnek, ha egy másik szál módosítja esetleg a this.wrapper referenciát :)

A másik, hogy bár sokan úgy gondolják, hogy nem kell (általában fogalmuk sincs miért), hogy a változó (ebben az esetben a wrapper) volatile legyen, én mégis azt javaslom, hogy tedd volatile-ra.
Mutasd a teljes hozzászólást!

  • Illetve a szálbiztosság kérdése mellet - gondolom egyértelmű, de - minden igyekezeted ellenére térhetsz vissza olyan clienttel, amihez a kapcsolódó token lejárt lesz a felhasználáskor. Sőt, már a visszatéréskor is. Ezt mivel idő alapú, nem fogod tudni kivédeni, és gondolom kezelned kell külön.
    Mutasd a teljes hozzászólást!
  • Köszönöm szépen, hasznos ötletek :)

    Ezt mivel idő alapú, nem fogod tudni kivédeni, és gondolom kezelned kell külön.

    Elvileg ezért csináltam azt, hogy magát az IClient interfészt implementáltam újra, így ki van kényszerítve, hogy azonnal használva is legyen az a kliens. Persze még így is van az a pár microsec, ami alatt ez összeeshet.
    Mutasd a teljes hozzászólást!
  • Egyrészt lehet, hogy pont azután 1 nanomásodperccel jár le a token, hogy visszaadtad. Ellenőrzéskor még valid, felhasználáskor már nem. Másrészt a szálak is felfüggesztésre kerülhetnek bárhol, bármeddig. Egy erősebb load alatt lévő gépnél ez lehet hosszú idő is. A hálózaton is kommunikálsz, az is hozzá fog adni. Amit csináltál, nagyon lecsökkenti a valószínűségét annak, hogy a token lejárt legyen, de nem zárja ki a lehetőséget. Az ilyen időalapon működő kódok sajnos nagyon törékennyé tudnak válni egy erősen túlterhelt gépen, ahol akár másodperces hiccupok is ki tudnak alakulni a magas load miatt.
    Mutasd a teljes hozzászólást!
  • Retry logikát már csináltam rá (decorator pattern az IClient-re, ami bizonyos exception-ök esetén újrapróbálja a request-et). Ezen túl tehetek még bármit is, vagy innentől kezdve a szerencse kérdése?
    Mutasd a teljes hozzászólást!
  • Elvileg lehet azzal még játszani, hogy lejárat előtt X idővel már új tokent kérsz, így tovább csökkentve a valószínűségét a lejáró tokennek. De nem hiszem, hogy megéri ezzel szórakozni. Már csak optimalizálni tudsz a szélsőséges esetekre, és a retry úgyis lekezeli.
    Mutasd a teljes hozzászólást!
  • Köszönöm szépen a segítségedet :)
    Mutasd a teljes hozzászólást!
abcd