Beiträge von Aquaatic

    eine Art Alternating Bit Protocol

    Ja, das hatte ich tatsächlich etwas im Hinterkopf. Ich sehe aber nicht, warum das nicht funktioneren sollte. Man muss aber auf jeden Fall etwas optimieren, im worst-case bei einer voll-vernetzten Topologie würde ich sonst auf n + n² Pakete pro normaler Änderung kommen, wobei n die Cluster-Größe ist. Ich würde mal behaupten, das Quadrat ist inakzeptabel bei häufigeren Änderungen.

    Mit einer Linien-Topologie sollte man das aber auf irgendwas in O(n) drücken können - zumindest in meinen Vorstellungen...

    Was meinst du genau mit dem ID-Bit?

    z.B. ein boolean, der irgendwo in deinem Knoten gespeichert ist. Wofür der genau gut ist, merkst du spätestens, wenn du mal versuchst, das ohne dieses in einem State-Chart zu modellieren:

    hier hätte ich das mal grob versucht. Problem ist: was, wenn ein Knoten jetzt aus consens schon wieder in wait zurückgekehrt ist und eine Änderung anfragt, ein anderer aber noch auf ein Ack eines anderen Knoten wartet? Dann geht ein Knoten in den conflict_enter-Zustand und bleibt dort hängen. Mit dem State-Bit kann man solche Situationen erkennen. Es wird stets dann geflippt, wenn eine Änderung vollzogen wurde. Dann kann man überprüfen, ob das state-Bit, das den Paketen beigefügt wird, bei den Zustandsübergängen im consens-Zustand dem lokal gespeicherten entspricht. Wenn nicht, bedeutet das dann, dass dieses Paket für eine spätere Änderung bestimmt ist (im Übrigen ist dann formal garantiert, dass die Veränderung, aufgrund der man sich gerade im consens-Zustand befindet, glücken wird, da ja ein Knoten offensichtlich schon alle acks erhalten hat).

    Jeder Zustand benötigt eine ID-Bit. Bevor der Zustand verändert wird, muss von allen Knoten diese Zustandsänderung abgenickt werden. Sollten weitere Änderungen gleichzeitig beantragt werden, so kann man das dann entsprechend mittels dieses ID-Bits erkennen. Dann kann man z.B. mittels Abstimmen oder mittels einer vorher festgelegten Knoten-ID die priorisierte Änderung festlegen (wichtig: vor Festlegung müssen sich dann alle Knoten in einen Ausnahmezustand bewegen, was wieder von allen bestätigt werden muss, bevor weiteres passiert, ansonsten sendet noch irgendein Knoten eine weitere Änderungsanfrage und bringt damit alles durcheinander), diese durchführen und die andere(n) wird/werden dann nach Rückkehr in den "Normalbetrieb" und einem entsprechenden Flip des Zustandsbits erneut beantragt.

    Das wäre vermutlich so die primitivste Lösung, die mir einfällt.

    Warum sich nicht einfach der Polymorphie bedienen?

    Provider -> Interface mit den benötigten Methoden

    InMemProvider -> ausschließlich Bereitstellung und Operationen auf lokal gespeicherten Daten

    DatabaseProvider -> hält InMemoryProvider im Hintergrund, der stets auf dem Stand vor dem letzten Update ist. Wie man die Veränderungs-Operationen hier implementiert, hängt vom konkreten Anwendungsfall ab. In jedem Fall wird aber bei einem fehlgeschlagenem Update dann der DatabaseProvider wieder auf den Stand des hinterlegten InMemoryProviders zurückgesetzt.


    Konkreter: Sollte man ohnehin bei jedem Update alles nochmal in die Datenbank schreiben, bietet es sich an noch einen zweiten InMemoryProvider zu hinterlegen, an den die Schreib-Operationen delegiert werden. Pseudo-Code-mäßig würde ich das dann so machen:


    Ist und bleibt aber irgendwie doch eine sehr primitive Sache. Eleganter wäre es irgendwie sowas wie Transaktionen einzuführen: Dein Provider bietet dann eine Methode transaction() an, die dann ein Transaktionsobjekt zurückgibt, das die entsprechenden Veränderungsoperationen anbietet, die aber noch nicht in das Provider-Objekt übernommen werden, solange die Transaktion (bzw. die darin vorgenommenen Änderungen) noch nicht vollständig in die DB geschrieben wurden. Das kann dann auch noch eventuelle Race-Conditions ausmerzen. Insbesondere wäre es dabei praktisch ein DBMS zu benutzen, das von sich aus sowas wie Transaktionen kennt. Aufpassen muss man dann nämlich wieder, sofern man sich in einem mehrfädigem Programm befindet, dass sich die Transaktionen nicht gegenseitig in die Quere kommen... Aber man muss ja auch nicht mit Kanonen auf Spatzen schießen, von daher ist das für Kleinkram wohl auch eher nicht nötig.

    Und vielleicht gibt es auch noch schlauere Lösungen, ich habe schon ewig nichts größeres "softwareiges" mehr programmiert, von daher bin ich da etwas raus und das waren nur so meine zwei ersten Ideen, die mir in den Sinn kamen...

    Okay, ich korrigiere mich: alle Jars in einen ClassLoader mittels des Arrays übergeben und dann in der entsprechenden Reihenfolge die Konstruktoren aufrufen

    Eleganter, nachdem UUID noch zum PK gemacht wurde:

    • add: INSERT INTO levelSystem (UUID, xp) VALUE (?, ?) ON DUPLICATE KEY UPDATE xp = xp + ?;
    • set: INSERT INTO levelSystem (UUID, xp) VALUE (?, ?) ON DUPLICATE KEY UPDATE xp = ?

    Das wird aber nicht funktionieren! Dein Plugin wird von Spigot gestartet, aber braucht das zu entpackende Plugin als Vorbedingung, die aber zu dem Zeitpunkt ja gar nicht erfüllt sein kann, weil es ja erst noch entpackt werden muss.

    Einfach das Plugin vorher mit in den Plugins-Ordner tun und gut ist - gerade wenn es privat genutzt wird, ist das ja völlig unproblematisch...


    Sollte deine API ohne Listener, Commands und ähnliches auskommen, kannst du sie auch als "Nicht-Plugin-Bibliothek" schreiben und mit in dein Plugin shaden (-> Maven), dann kannst du das aber immer nur genau ein mal pro Server verwenden, sonst verwirrst du den ClassLoader.

    Ja, okay, dann muss man sich überlegen, welche Daten nur für einen Server zu einer Zeit relevant sind / nicht verändert werden, während der Spieler auf dem Server ist. Den Rest müsste man dann immer abfragen. Falls die Performance wirklich so kritisch ist, kann man ja auch noch einen schnellen Cache (z.B. Redis) dazwischenschalten.

    Und dann noch vielleicht eine Warnung: bevor ein Multiproxysystem wirklich praktikabel ist, braucht man mehrere tausend Spieler gleichzeitig auf dem Server, davor kann man das ganze noch wunderbar auf einem entsprechend dimensionierten Bungee laufen lassen. Also lieber klein anfangen und wenn es dann mal darauf zugeht wirklich einen ganzen Server mit einem Bungee ausreizen, DANN kann man sich darüber Gedanken machen. Davor ist das nur unnötiger Aufwand und zusätzliche Fehleranfälligkeit für keinen Vorteil.

    Zu Frage 2: Wichtig ist vielleicht die Überlegung, dass ein Spieler sich zu einer Zeit immer nur auf einem Server befindet (und dem Proxy, aber da werden höchstwahrscheinlich keine konkurrierenden Situationen auftreten, oder?). Das heißt, dass man beim Joinen des Spielers einmal die Daten laden und beim Verlassen die Daten in die Datenbank schreiben kann. Man muss dann nur sicherstellen, dass die Daten geschrieben wurden, bevor der Spieler auf den nächsten Server geschoben wird.

    Ein weiterer Tipp: deine Datenbank kann auch rechnen! Anstatt erst die Stats zu erfragen (ich denke das macht getStatsReset), kannst du auch einfach deine Query in UPDATE SpeedCW SET STATSRESET = STATSRESET - ? WHERE UUID = ? abändern und in den ersten Parameter die abzuziehenden Punkte und in den zweiten die UUID setzen

    Die Art der Speicherung ist OK, u.U auch eine der performantesten... wie viele Zeilen hat die Tabelle denn? Sind Indizes gesetzt (hier insbesondere auf UUID)? Mal versucht zu "benchmarken", wie lange die Abfrage braucht? Zum Beispiel so:

    Code
    long start = System.currentTimeMillis();
    // DB-Abfrage
    System.out.println("DB-Abfrage hat " + (System.currentTimeMillis() - start) + "ms gedauert");

    Ich habe mir mal eben (tatsächlich seit Jahren mal wieder) einen kleinen Testserver eingerichtet und das ganz mal ausprobiert. Meine Befürchtung war:


    Spieler betritt den Server -> PostLoginEvent wird aufgerufen -> es wird festgestellt, dass Server voll -> p.disconnect(...) wird aufgerufen -> DisconnectEvent wird aufgerufen ohne, dass Spieler vorher in die playerIps Map eingetragen wurde -> nichts passiert -> LoginEvent wird fortgesetzt -> Spieler wird in playerIps Map eingetragen und nie wieder entfernt, weil sein DisconnectEvent schon aufgerufen wurde. Dadurch kann der Speicher für den Spieler nie freigegeben werden und man hat so einen Memory-Leak erzeugt.


    Das habe ich mal "simuliert", indem ich einfach mal

    • einen Listener fürs PostLoginEvent, der den Spieler disconnected, dann 5 Sekunden blockiert und schließlich eine Nachicht ausgibt und
    • einen Listener fürs DisconnectEvent, der einfach nur eine Nachicht ausgibt,

    geschrieben habe. Wenn die Nachicht im DisconnectEvent also vor der im PostLoginEvent geschrieben worden wäre, hätte sich meine Befürchtung bestätigt. Hat sie aber nicht. Das DisconnectEvent wurde erst nach vollständiger Ausführung des PostLoginEvents aufgerufen, hier ist man also (zumindest ist das jetzt experimentell bestätigt) auf der sicheren Seite.


    Mit dieser Sicherheit kann man den Code nochmal optimieren und sich die zweite Map doch sparen:

    Im PostLoginEvent wird der Zähler für den Spieler immer - egal, ob damit eine Grenze überschritten wird oder nicht - erhöht und im DisconnectEvent immer verringert.

    Wenn die Grenze im PostLoginEvent überschritten wird, wird der Spieler aber natürlich trotzdem disconnected. Zwar ist der Zähler dann nämlich erhöht, aber dadurch wird ja das DisconnectEvent aufgerufen und der Zähler wieder verringert, man befindet sich also wieder im Ausgangszustand, als hätte sich der Spieler nie verbunden. Das gilt dann genauso für alle anderen Disconnects im PostLoginEvent.

    In Code also:

    Noch eine Idee: was spricht denn dagegen, einfach ein neues Event einzuführen, das immer dann gefeuert wird, wenn das JoinEvent gefeuert wurde und die die Einstellungen übermittelt wurden? Davor ggf. noch wieder in den Main-Thread mit BukkitScheduler#runTask(...) eintreten, dann kann muss man sich auch um Parallelität keine Sorgen machen.

    In dem Event kann man dann alles ganz normal machen wie du es im JoinEvent machen willst. Das wäre so das idiomatischste, was mir einfällt.