Zeitversetzte "Aktion"

Hallo zusammen,

ich bin es wieder mit einem neuen Problem bezüglich dieser Zeitverzögerung.
Wie Löst ihr das, wenn der User nicht online ist?

Also z.b.

User startet mit dem Bauen, in die DB wird gespeichert, dass er in 5 Minuten fertig wäre und geht dann offline.
Klar, sobald er wieder online geht, wäre das Haus gebaut. Aber die "Rohstoffe" die das Haus bis dahin gebracht / verbraucht hätte fehlen. Errechnet Ihr die dann nachher, sobald der User wieder online kommt?

Aber wie ist das, bei Aktionen, die einen anderen User betreffen?
Schicke Soldat A nach User X2 und keiner von den beiden kommt mehr Online. Dann wird der Kampf unter Umständen erst Tage später und die Punkte / Rohstoffe vollkommen falsch berechnet..

Gruß
paD
 
Hi

Ich kenn die Details nicht, aber da wäre vllt. ein dauerhaft laufendes Programm
am Server sinnvoller? Statt normale PHP-Seiten, die immer nur durch
den Browser gestartet werden.
 
Huhu,

meinst du jetzt Cronjob-Mäßig oder wirklich ein eigenes Programm schreiben, was solche Aufgaben löst?

Gruß
paD
 
Meinte wirklich dauerhaft.
Ohne mehr Details zum Spielsystem kann man das aber schwer abschätzen.

Gehostete Cronjobs sind bei vielen/wechselnden zeiten ziemlich unflexibel und sind bei kurzen Intervallen auch mehr Serverbelastung (als Eigenprogramm).

Eigenes, dauerhaftes Programm...da stellt sich die Frage,
wieviel Kontrolle hast du über den Server?
 
Ist ein Rootserver, also das sollte kein Problem sein.
Allerdings beherrsche ich wohl keine andere Programmiersprache außer PHP so gut, dass es nicht monate dauern würde, bis ich ein Programm geschrieben habe, dass das beherrscht ;-)

Ich könnte vielleicht ein PHP-Socketserver schrieben, der die Aufgaben erledigt. Wäre wahrscheinlich immer noch performanter als nen Cronjob oder so ;-)

paD
 
Wozu brauchst du einen Socket-Server wenn da "nur" Aufträge aus einer DB geholt und entsprechend verarbeitet werden sollen? Sowas kann man prima in PHP machen. Bau dir ein Script, was die SQL-Statements in einer Endlosschleife ausführt und ggf. ein paar Sek. wartet. Das Script kann eine permanente Connection zur DB haben, sollte aber darauf reagieren, wenn der DB-Server mal nicht mehr verfügbar ist. Ich würde das mit PDO und Exception-Handling lösen. Dazu ein paar Prepared Statements um die DB zu entlasten.

Könnte ungefähr so ablaufen:

PHP:
#!/usr/bin/php -r

$db = new PDO('mysql:dbname=datenbankname;host=127.0.0.1', 'user', 'passwort');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Hier Prepared Statements vorbereiten
$auftraege_statements = $db->prepare("SELECT * FROM auftraege WHERE auftragsstatus <> 'fertig'");

// Endlosschleife
while(true)
{
  try
  {
    $auftraege_statements->execute();
    
    // jetzt die Aufträge aus dem Result lesen und verarbeiten
    while($auftraege = $auftraege_statements->fetchObject())
    {
      // Hier sonst wie verarbeiten...
    }

    // Erstmal 2 Sek. schlafen...
    sleep(2);
  }
  catch(PDOException $pdoex)
  {
    // Exception loggen...
    //Logger::log($pdoex);
    // Sicherheitshalber die Datenbankverbindung neu aufbauen, Prepared Statements müssen auch neu initialisiert werden
    $db = new PDO('mysql:dbname=datenbankname;host=127.0.0.1', 'user', 'passwort');
    $auftraege_statements = $db->prepare("SELECT * FROM auftraege WHERE auftragsstatus <> 'fertig'");
  }
  catch(Exception $ex)
  {
    // Exception loggen...
    //Logger::log($ex);
  }
}

Das Script kannst du einfach in eine Datei "auftraege.php" schreiben. Diese Datei machst du ausführbar:

Bash:
chmod 0755 auftraege.php

Anschließend kannst du das Script starten und in den Hintergrund legen:

Bash:
./auftraege.php 2>&1 >/var/log/auftraege.log &

Das Beispiel setzt voraus, das dein Root-Server ein Linux-System ist. Unter Windows wäre das etwas anders zu bewerkstelligen.
 
Wow, tausend tank..das ist ziemlich genial.
Allerdings muss ich gestehen, dass ich das Script nicht 100% verstehe, da ich mich mit der PDO-Klasse noch nie beschäftigt habe.

Wieso z.b. haut das nicht 100% auf die CPU?
Sorgt execute() dafür, dass die While angehalten wird, bis was in die DB geschrieben wird?
Oder ist das "nur" das sleep() was das Script quasi 2 Sekunden anhält..?

Und hättest du vielleicht noch eine Lösung parrat, wie das Script automatisch bei einem Server Neustart gestartet wird?

Besten Dank nochmal!

paD
 
Allerdings muss ich gestehen, dass ich das Script nicht 100% verstehe, da ich mich mit der PDO-Klasse noch nie beschäftigt habe.

Kein Problem, kann man ja auseinander nehmen und über die einzelnen Sachen diskutieren ;)

Wieso z.b. haut das nicht 100% auf die CPU?

Das hängt mal sehr stark davon ab, was die SQL-Statements genau tun. Dass das Script nicht auf die Datenbank hämmert, wird durch den sleep() bewerkstelligt.

Sorgt execute() dafür, dass die While angehalten wird, bis was in die DB geschrieben wird?

Naja, nein. execute() führt das Prepared Statement aus. Es funktioniert ungefähr so:

- Beim erstellen des Prepared Statements wird der SQL-Server mit dem Query gefüttert, was er sich merkt.
- Im Statement können variable Teile enthalten sein, dazu kommen wir, wenn du es brauchst ;)
- Der execute()-Aufruf teilt dem SQL-Server mit "Achtung, führe jetzt das Statement aus, was du dir gemerkt hast, zusammen mit variablen Werten, falls ich dir welche mitgeteilt habe (siehe Punkt 2)".

Oder ist das "nur" das sleep() was das Script quasi 2 Sekunden anhält..?

Sollte bereits oben beantwortet worden sein.

Und hättest du vielleicht noch eine Lösung parrat, wie das Script automatisch bei einem Server Neustart gestartet wird?

Meinst du ein Start-/Stop-Script für die Runlevel-Verlink (init/upstart/$whatever)? Kommt darauf an, was du für ein System hast. Debian/Ubuntu lösen das anders als z.B. Fedora oder SuSE.
 
Tausend Dank für die rasche Antwort.

Auf dem Server läuft Debian. Hatte das auch mal bei meinem TeamSpeak-Server gemacht, dass er automatisch startet, nachdem man mal ein Neustart für Updates oder dergleichen gemacht hat. Wenn das so funktioniert, wäre das kein Problem ;-)

Wie würde das denn aussehen, wenn ich Variablen bräuchte..?
Wenn ich an User-Funktionen denke, brauche ich sicherlich User-Aufgaben und Aufgaben geplant ach Zeit / Tag.. Variablen wären nicht schlecht :)

paD
 
Grundsätzlich funktioniert das genau wie bei deinem Teamspeak-Server. Allerdings würde ich das Script für diesen Umstand noch etwas aufbohren. Ich würde das so machen:

- Start-/Stop-Script startet das Script wie in meinem ersten Beispiel beschrieben
- Script schreibt seine Prozess-ID nach /var/run/auftraege.pid, sobald es gestartet wurde (siehe http://php.net/manual/de/function.getmypid.php); Dafür kannst du ganz normal fopen, fwrite und fclose verwenden.
- Stop-Teil des Start-/Stop-Scripts liest /var/run/auftraege.pid aus, um an die Prozess-ID zu kommen, führt einen "kill -9" mit der PID aus und löscht das PID-File wieder. Hier ein Beispiel:

Bash:
#! /bin/sh
### BEGIN INIT INFO
# Provides:          auftraege
# Required-Start:    $local_fs $remote_fs
# Required-Stop:     $local_fs $remote_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Startet das PHP-Script für die Aufträge
# Description:       This file should be used to construct scripts to be
#                    placed in /etc/init.d.
### END INIT INFO

# Author: Foo Bar <foobar@baz.org>
#
# Please remove the "Author" lines above and replace them
# with your own name if you copy and modify this script.

# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Aufträge-Dienst"
NAME=auftraege
DAEMON=/home/www/$NAME.php
DAEMON_ARGS="--options args"
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions

#
# Function that starts the daemon/service
#
do_start()
{
        # Return
        #   0 if daemon has been started
        #   1 if daemon was already running
        #   2 if daemon could not be started
        start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
                || return 1
        start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
                $DAEMON_ARGS \
                || return 2
        # Add code here, if necessary, that waits for the process to be ready
        # to handle requests from services started subsequently which depend
        # on this one.  As a last resort, sleep for some time.
}

#
# Function that stops the daemon/service
#
do_stop()
{
        # Return
        #   0 if daemon has been stopped
        #   1 if daemon was already stopped
        #   2 if daemon could not be stopped
        #   other if a failure occurred
        start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
        RETVAL="$?"
        [ "$RETVAL" = 2 ] && return 2
        # Wait for children to finish too if this is a daemon that forks
        # and if the daemon is only ever run from this initscript.
        # If the above conditions are not satisfied then add some other code
        # that waits for the process to drop all resources that could be
        # needed by services started subsequently.  A last resort is to
        # sleep for some time.
        start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
        [ "$?" = 2 ] && return 2
        # Many daemons don't delete their pidfiles when they exit.
        rm -f $PIDFILE
        return "$RETVAL"
}

#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
        #
        # If the daemon can reload its configuration without
        # restarting (for example, when it is sent a SIGHUP),
        # then implement that here.
        #
        start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
        return 0
}

case "$1" in
  start)
        [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
        do_start
        case "$?" in
                0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
                2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
        esac
        ;;
  stop)
        [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
        do_stop
        case "$?" in
                0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
                2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
        esac
        ;;
  #reload|force-reload)
        #
        # If do_reload() is not implemented then leave this commented out
        # and leave 'force-reload' as an alias for 'restart'.
        #
        #log_daemon_msg "Reloading $DESC" "$NAME"
        #do_reload
        #log_end_msg $?
        #;;
  restart|force-reload)
        #
        # If the "reload" option is implemented then remove the
        # 'force-reload' alias
        #
        log_daemon_msg "Restarting $DESC" "$NAME"
        do_stop
        case "$?" in
          0|1)
                do_start
                case "$?" in
                        0) log_end_msg 0 ;;
                        1) log_end_msg 1 ;; # Old process is still running
                        *) log_end_msg 1 ;; # Failed to start
                esac
                ;;
          *)
                # Failed to stop
                log_end_msg 1
                ;;
        esac
        ;;
  *)
        #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
        echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2
        exit 3
        ;;
esac

Das Script ist aus dem /etc/init.d/skeleton kopiert und muss ggf. weiter angepasst werden ;)

Zu den Variablen. Dazu gibts bei den Prepared Statements Platzhalter. Nehmen wir wieder unser Beispiel von oben:

PHP:
#!/usr/bin/php -r
 
$db = new PDO('mysql:dbname=datenbankname;host=127.0.0.1', 'user', 'passwort');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
 
// Hier Prepared Statements vorbereiten
$auftraege_statements = $db->prepare("SELECT * FROM auftraege WHERE auftragsstatus <> 'fertig'");

// Hier wird ein Platzhalter verwendet
$beenden_statement = $db->prepare("UPDATE auftraege SET auftragsstatus = 'fertig' WHERE id = ?");
 
// Endlosschleife
while(true)
{
  try
  {
    $auftraege_statements->execute();
    
    // jetzt die Aufträge aus dem Result lesen und verarbeiten
    while($auftraege = $auftraege_statements->fetchObject())
    {
      // Wenn der aktuell ausgelesene Auftrag bereits 10 Minuten (60 Sek mal 10 Min) gelaufen ist, lassen wir ihn beenden:
      if($auftraege->finish_time - auftraege->start_time > 10 * 60)
      {
        $beenden_statement->bindValue(1, $auftraege->id);
        $beenden_statement->execute();
      }
    }
 
    // Erstmal 2 Sek. schlafen...
    sleep(2);
  }
  catch(PDOException $pdoex)
  {
    // Exception loggen...
    //Logger::log($pdoex);
    // Sicherheitshalber die Datenbankverbindung neu aufbauen, Prepared Statements müssen auch neu initialisiert werden
    $db = new PDO('mysql:dbname=datenbankname;host=127.0.0.1', 'user', 'passwort');
    // Prepared Statements erneuern
    $auftraege_statements = $db->prepare("SELECT * FROM auftraege WHERE auftragsstatus <> 'fertig'");
    $beenden_statement = $db->prepare("UPDATE auftraege SET auftragsstatus = 'fertig' WHERE id = ?");
  }
  catch(Exception $ex)
  {
    // Exception loggen...
    //Logger::log($ex);
  }
}
 
Zurück