Records worden soms te laat opgeslagen

  • Hallo,


    Ik heb het volgende probleem:
    De records worden soms niet op tijd opgeslagen wanneer een gebruiker erg snel handelingen uitvoerd.


    Stel gebruiker Henk heeft 1.000 op zak, en klikt rustig op een bepaalde optie (Die bijvoorbeeld 100 kost) en gaat de gebruiker steeds 100 omlaag.
    Maar wanneer dit erg snel gebeurt is soms de check of je meer als 100 hebt geldig, terwijl de speler vervolgens op een negatief saldo komt.


    Hierboven even een kort voorbeeldje van de situatie.


    Er word gebruik gemaakt van een MyISAM tabel in MySQL.
    Het veld dat word aangepast betreft een INT veld.
    Er word gebruik gemaakt van een VPS, dus alle settings zijn aan te passen in de configs.


    Er is maar 1 persoon actief op een account, dus er zouden niet meerdere aanvragen tegelijk moeten komen.
    Echter als een speler erg snel de pagina gaat vernieuwen lijkt het erop dat deze soms tegelijk worden uitgevoerd.


    Zelf vermoed ik dat het het mogelijk aan locking (Of het gebrek aan) ligt, of dat het ophalen van de gebruikersgegevens word gecached.
    $gebruikerinfo is een verzameling van gegevens die uit de database word gehaald aan het begin van de pagina laden, is het mogelijk dat deze read opdracht word gecached omdat het in dezelfde seconde (Of erg korte periode) plaats vind als de vorige aanvraag ?


    Hoe zou ik het beste het probleem kunnen vinden ?
    En waarop zou ik kunnen zoeken om tot de juiste functie of instelling te komen om dit probleem op te lossen ?


    Alvast bedankt,
    Pekelterror

  • Guest, wil je besparen op je domeinnamen? (ad)
  • Ah, een klassiek voorbeeld van een gebrek aan ondeelbaarheid van (database-)operaties.


    Dit heeft niet zozeer te maken met de manier van programmeren (deze doet er echt niet toe) maar meer met hoe je omgaat met je database. Je hebt hiervoor echt een soort van locking mechanisme nodig en idealiter gebruik je hiervoor transacties. Jammergenoeg worden (database-)transacties niet ondersteund door MyISAM.


    Ik weet verder niet wat voor applicatie dit betreft (mafia game?) maar meestal hebben deze alle kenmerken van een "administratief" systeem - een database met een veelvoud aan tabellen met daartussen allerlei verbanden.


    Wat dat betreft snap ik sowieso niet waarom je daarvoor ooit MyISAM zou willen gebruiken omdat er met die engine geen onderlinge afstemming tussen tabellen kan plaatsvinden. Je kunt dan eigenlijk met geen goed fatsoen verwachten dat alle data in die tabellen (onderling) kloppend is en (onderling) kloppend blijft. Alle data hangt letterlijk als los zand aan elkaar. Voor een administratief systeem ben je veel beter af met InnoDB. Die heeft namelijk al deze voorzieningen (transacties, foreign keys et cetera) wel.


    Wat natuurlijk ook niet meehelpt is dat je op verschillende manieren refereert aan het saldo contant geld. Je hebt $gebruikerinfo->contant, een property van een of ander object? en users.contant. En dan zit het user id blijkbaar in $ingame, die zou ik dan eerder verwachten in $gebruikerinfo->id ofzo. Ik heb een onderbuikgevoel van spaghetticode.


    Dan speelt er mogelijk nog het volgende: je refresht de pagina waarschijnlijk niet direct, anders zou dit minder snel (maar nog steeds, het is geen remedie) voor problemen zorgen, dus waarschijnlijk ben je tegelijkertijd een of ander formulier of andere data aan het verwerken terwijl je vrolijk een pagina aan het uitdraaien bent. Ben ik een beetje warm? Je bent dus waarschijnlijk zogenaamde "state changes" on-the-fly aan het uitvoeren wat uit algemeen oogpunt van programma-ontwerp een heel slecht idee is.


    Betreft dit legacy code of ben je dit op dit moment aan het ontwikkelen?

  • @FangorN Bedankt voor je bericht.
    Het beste zou ik dus kunnen omschakelen van MyIsam naar InnoDB ?
    En dan transacties gebruiken ?


    Het betreft inderdaad een mafia game.


    $gebruikerinfo is een fetch_object van een X aantal velden die uit de gebruiker worden opgehaald van de user.
    Dit omdat we elke aanvraag een X aantal sowieso al nodig zijn.


    $ingame bevat de session user (escaped) zodat ik deze makkelijk overal kan gebruiken.
    Dit zou uiteindelijk dus $gebruikerinfo->user kunnen worden, maar die waarde staat toch gelijk aan $ingame aangezien $ingame (Oftewel de $_SESSION['user'] escaped) word gebruikt in de Select query die word gebruikt bij $gebruikerinfo.


    We zien door de logging dat er soms 5 aanvragen per seconde worden gedaan naar de pagina die dit probleem geeft.
    Er word niet in andere formulieren tegelijk gewerkt, maar de verwerking gaat vermoedelijk te langzaam voor de volgende aanvraag weer komt.


    Het bevat wel veel legacy code aangezien er op een eerder project is verder gewerkt dat ik jaren terug heb gemaakt.
    Had alleen niet nagedacht over de types engines van de database en locking, die was ik (in mijn ogen) nooit nodig, maar zal er toch eens naar kijken.
    Gelukkig kan ik die altijd nog aanpassen, zal alleen even moeten kijken of MyIsam naar InnoDB geen problemen oplevert (Maar verwacht ik niet).


    @Luc Bedankt voor het meedenken.
    Alleen zou dit het probleem erger maken.
    Nu gaat de gebruiker na een tijdje in de min, tot de database weer bij is, dit moeten ze dan weer aanvullen tot ze in de plus zitten.
    Met jou kleine fix zou juist de afschrijving misschien nog een keer minder gebeuren.


    TLDR; Ik zal me eens inlezen op het verschil van InnoDB/MyIsam, een locking mechanisme en transacties.
    Hebben jullie (Indien er meerdere soorten zijn), nog een locking mechanisme of transactie methode die jullie persoonlijk graag gebruiken, of zouden aanraden in deze situatie ?

  • $ingame bevat de session user (escaped) zodat ik deze makkelijk overal kan gebruiken.

    Oef. Ik weet niet of ik dat zou doen. Dit klinkt namelijk sterk als "escape on input". En escaping van data hangt af van de context. Waarvoor is dit (op voorhand) ge-escaped dan? Voor SQL? Voor HTML? Voor alles?


    Indien het de bedoeling is dat data ontdaan moet worden van een mogelijk speciale betekenis in een bepaalde context, dan doe je dat tijdens het gebruik van deze data in die context. Het is zaak dat je output escaped.


    Nu is het nogal ongewis dat $ingame ge-escaped is (en voor welke context precies). Als je dit altijd consequent doet tijdens het gebruik dan is dat compleet ondubbelzinnig en hoef je niet elke keer na te denken of na te gaan of dit wel het geval was. Nu moet je elke keer nadenken "is dit veilig of niet". Maak het jezelf makkelijk en escape gewoon consequent alle DATA in je SQL. Of maak gebruik van prepared statements ofzo, in welk geval je beter PDO kunt gebruiken want prepared statements in MySQLi zijn nogal een gribus.


    Trouwens, de enige informatie die een sessie qua user data hoeft te propageren is in principe het user id. Alle actuele user data zou elke page request opnieuw berekend moeten worden om ervoor te zorgen dat deze altijd up-to-date is. Die zou je dan in een user object kunnen stoppen waar je vervolgens aan refereert bij gebruik in je code, zoals je nu ook al doet met via $gebruikerinfo.


    Ik weet niet hoeveel user data er nu in de sessie zit? Als je op die manier gebruikersgegevens tussen requests propageert introduceer je een synchronisatieprobleem. Immers, elke keer dat er iets in de user data verandert zou je dit moeten updaten in je sessie. En stel nu dat een admin rechten van een (andere) gebruiker intrekt, wanneer wordt dit dan gereflecteerd in de sessie van die gebruiker zelf? Je kunt namelijk niet rechtstreeks andermans sessie inhoudelijk wijzigen.


    Bedien je van het motto: filter input, escape output, en je komt een heel eind. Mits je weet wat dit inhoudt :p.


    Escape-on-input is heel vaak geen goed idee, maar zoals met alles zijn er ook uitzonderingen. Denk bijvoorbeeld aan "session flash messages". Stel dat je na het bijwerken van een artikel "bladibla" iemand direct redirect naar het overzicht van artikelen, maar dat je wel op een of andere manier een terugkoppeling wilt geven dat het artikel succesvol is gewijzigd. Deze informatie zou je tijdelijk in (een subarray van) je sessie kunnen stoppen waarin het bericht "artikel 'bladibla' succesvol bijgewerkt" staat. Je weet in dat geval dat dit bericht in de HTML-context gebruikt gaat worden, en je wilt niet dat je complete pagina breekt als je voor de gein </div> in je titel zet ofzo. Dus in dat geval is het geoorloofd om op voorhand output te escapen. Maar dit is dus een uitzondering op de regel.


    Als je niet op voorhand weet in welke context data gebruikt gaat worden heeft het ook geen zin om te escapen en is het ook niet verstandig om deze data op voorhand voor alle mogelijke contexten onschadelijk te maken. Dit kan zelfs fouten in de hand werken zoals het bovenstaande artikel mooi illustreert :).

  • Het is inderdaad escape on input.
    $ingame bevat een session waarde (Die gezet word bij het inloggen, dus zou ivm een match al veilig moeten zijn).


    Ondanks dat escape ik het toch liever voor het gebruik van de querys.

    PHP
    <?php
    $ingame = mysqli_real_escape_string($connectie, $_SESSION['user']);


    Deze $ingame word verder niet gebruikt om het te tonen, dat gaat eigenlijks via het $gebruikerinfo 'object' (een fetch_object van een query), en deze bevat dan een heleboel waardes die vaak worden gebruikt zoals ze in de database staan van de gebruiker.


    Ik ben in elk geval alvast overgestapt op InnoDB, maak gebruik van SQL_NO_CACHE in de select query voor $gebruikerinfo om te zorgen dat ik de meest recente versie heb, en heb enkele onnodige querys verwijderd uit de scripts in de hoop dat het daardoor minder zwaar is.
    Verder schrijf ik nu wel wat debug weg om te kijken of dit probleem zich nog voordoet.


    Het lijkt op dit moment goed te gaan.
    Mocht het toch nog weer gebeuren zal ik je advies over Locking en Transacties (aangezien we nu toch InnoDB gebruiken) beter bestuderen.


    Bedankt voor de hulp!

  • Data die in je systeem zit zou je niet als veilig moeten zien.


    Stel dat, om wat voor reden dan ook, gebruikersnamen of andere teksten van een profield de input "</div>" toestaan. Als je dit vervolgens zonder escaping weergeeft ligt je layout in puin. En zo zijn er nog wel andere zaken denkbaar zoals AJAX-calls naar een externe site als jij een profiel bekijkt ofzo... Escape altijd output, tenzij je een speciale, en gedocumenteerde, reden hebt om dat niet te doen.


    En om toch terug te komen op het voorgaande, vergelijk:

    PHP
    $sql = "SELECT something FROM table WHERE condition='".$condition."'";

    De vraag is dan elke keer opnieuw: is $condition gevalideerd / veilig? Of beter gezegd: is dat een query die altijd het gewenste resultaat geeft? Iedereen die dat stuk code ziet zou zich dat af moeten vragen. Dat kost elke keer weer (interpretatie)tijd.


    Vergelijk dit met:

    PHP
    $sql = "SELECT something FROM table WHERE condition='".$db->escape($condition)."'";


    Waarbij $db->escape() een shorthand is voor real_escape_string(). Het maakt in dat geval niet uit of $condition veilig was voor gebruik in een query of niet - de combinatie escaping-functie + quotes garandeert dit. Op een soortgelijke wijze zou je prepared statements kunnen gebruiken, ook dat is een methode om te waarborgen dat DATA niet als SQL geinterpreteerd kan worden (dit is in feite wat SQL-injectie mogelijk maakt).


    Het dilemma in het eerste fragment is dus altijd of escaping bewust achterwege is gelaten of per ongeluk is vergeten. Je introduceert hiermee twijfel. Als je gewoon consequent alles escaped is er NOOIT ruimte voor twijfel. Zodat je hier dan ook nooit meer over na hoeft te denken.

  • Bedankt FangorN.


    In mijn geval is $ingame altijd geescaped, dit gebeurt al in de config file die overal word toegevoegd (En zonder die is er sowieso geen database connectie mogelijk).
    Ik snap dat op de locatie zelf escapen altijd zorgt dat het goed is en het is gelijk zichtbaar (Mochten er later anderen in de code kijken).
    Maar in dit geval word $ingame op slechts 1 locatie aangemaakt en is daar gelijk geescaped tegen injectie.


    Wanneer ik andere waardes gebruik in een query worden die wel in de query zelf geescaped, maar aangezien $ingame in elk scripts meerdere keren word gebruikt leek me escapen op 1 locatie makkelijker en kan het daardoor ook niet vergeten.
    Maar je uitleg is wel logisch kan het misschien beter op de plek zelf toepassen zodat het duidelijker is.

  • en kan het daardoor ook niet vergeten

    Totdat je dit wel een keer doet :p. Het is gewoon wachten op een ongeluk.


    Maar je uitleg is wel logisch kan het misschien beter op de plek zelf toepassen zodat het duidelijker is.

    Het is gewoon vele malen consequenter. Plus je hoeft er dan gewoon niet meer over na te denken, dus het is ook gewoon stukken simpeler (ook een goed ontwerpprincipe: Don't Make Me Think). Escape waarden op het moment dat je deze in een context onschadelijk wilt maken. Maar niet vantevoren.


    Als je gewoon deze laatste aanpak gebruikt weet je meteen wanneer iemand vergeten is een waarde te escapen, je hoeft dan niet eens de afweging te maken op grond van de inhoud van de variabele, escape gewoon altijd, maar dan dus wel op het goede moment :).


    Hetzelfde geldt voor de context waarbinnen de waarde ontdaan moet worden van een mogelijk speciale betekenis: wat "speciaal" is hangt van de context af - deze hoeft ook niet altijd op voorhand vast te staan. Zou je dan zo'n variabele maar op voorhand dood moeten escapen voor alle mogelijke contexten? En wat dan als je een dump wilt doen naar een plaintext bestand, ga je dan weer alles un-escapen (denk aan "& amp;" enzo, ga je die weer terugvertalen naar "&"?). Je doet dan waarschijnlijk een hoop werk voor niets, werk waar je je later mogelijk mee in de vingers snijdt.


    Ah well.

Participate now!

Heb je nog geen account? Registreer je nu en word deel van onze community!