Ankündigung

Einklappen
Keine Ankündigung bisher.

Cronjob, minütlich & mehrfache Verarbeitung verhindern...

Einklappen

Neue Werbung 2019

Einklappen
X
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

  • Cronjob, minütlich & mehrfache Verarbeitung verhindern...

    Hallöchen,

    ich konnte mit Google zwar unendlich viel zum Thema "Cronjob ausführen/erstellen" finden aber dies trifft meine Frage nicht wirklich.
    Denn den Cronjob auszuführen ist kein Problem, es geht mehr darum das er jede Minute ausgeführt wird und ich nicht weiß wie ich dies mit der Datenbank synchron halten soll...

    Als Beispiel nehmen wir Newsletter die deutlich länger als 1 Minute beschäftigt sind.
    Das Problem ist, das sich die Prozesses überschneiden und Datenbank-Salat entsteht....


    Eine separate Tabelle wo ich erst mal die abzuarbeitenden Datensätze ablegen, verschiebt das Problem nur (erster Durchlauf erfasst die Datensätze und trägt sie in der Tabelle ein, der nächste Durchlauf kann diese dann laden aber selbes Problem da die Verarbeitung länger dauert = Überschneidung).

    Toll wäre wenn ich mit TRANSACTION iswas hin bekäme, das z.B. 1000 Datensätze gesperrt werden, dann als "in Bearbeitung" markiert werden und dann exakt diese als select zurück erhalte damit php seine Arbeit machen kann...
    Das Problem ist wieder die Überschneidung, während eine Instanz 1000 Datensätze sperrt und verarbeitet, kann eine andere Instanz trotzdem selektieren aber bin jetzt auch nicht so erfahren mit TRANSACTIONs

    Ja, irgend wie komisch, vllt. hat ja Jemand ein Tipp für mich.


    MfG: Paykoman

  • #2
    Eine Möglichkeit wäre den Skriptstart und Ende serverseitig zu speichern (DB, File,..) und beim jeden Skriptstart per Cron abzufragen ob noch ein Skript aktiv ist. Läuft noch eine Instanz, wird das Skript beendet bevor irgend eine weitere Aktion ausgefühert wird.
    Wird das geschickt mit einem Zeitstempel kombiniert, dann können so selbst Blockierungen behandelt werden wo durch einen irregulären Skriptabbruch dieser nicht ordnungsgemäß sein Ende markiert.

    Kommentar


    • #3
      Schau dir mal die Lock Component von Symfony an, diese kannst du auch Standalone verwenden, also ohne das ganze Framework.
      https://symfony.com/doc/current/components/lock.html

      Kommentar


      • #4
        anacron macht das wohl automatisch, sonst sollte eine datei in /tmp/lock reichen, wei man das halt so macht....
        Wird das geschickt mit einem Zeitstempel kombiniert, dann können so selbst Blockierungen behandelt werden wo durch einen irregulären Skriptabbruch dieser nicht ordnungsgemäß sein Ende markiert.
        eine idee, die andre ist in die lock datei ne pid zu schreiben, und wenn die drinsteht ggf den prozess zu intersuchen.

        Kommentar


        • #5
          Guten Morgen,

          schon mal danke für eure Vorschläge, damit kam ich letztlich auf eine neue Idee...

          Also, ich habe eine übergeordnete Klasse die per cronjob angesteuert wird und alles in die Wege leitet...
          Diese hat nochmals eine übergeordnete Datei, die, die Abhängigkeiten lädt/bereitstellt und die Ausführung der Datei prüft ggf. unterbindet etc. (diese Datei sparen wir uns aber)

          htdocs/cronjob.php
          htdocs/fw/class/cronjob.php
          PHP-Code:
          // Autoloader (übergeordnete Datei) aufrufen:
          // 'php /var/www/vhosts/domain.de/htdocs/cronjob.php task:minit'
          // führt am Ende dann diese Klasse aus mit der value nach dem task:{$job}

          class cronjob{
              function 
          __construct($job){
                  
          // vorläufig hardcoded, später aus der DB laden
                  
          $jobs = [
                      
          'minit' => ['auction'],
                      
          'hourly' => [],
                      
          'daily' => [],
                      
          'aucWorker' => 'auction',
                      
          'aucWorker2' => 'auction'
                  
          ];

                  
          // which time period must be executed
                  
          switch( $job ){
                      case 
          'minit':
                          foreach( 
          $jobs['minit'] as $plugin ){
                              
          ModelFactory::init($plugin'cron'$job);
                          }
                          break;

                      case 
          'hourly':
                          foreach( 
          $jobs['hourly'] as $plugin ){
                              
          ModelFactory::init($plugin'cron'$job);
                          }
                          break;

                      case 
          'daily':
                          foreach( 
          $jobs['daily'] as $plugin ){
                              
          ModelFactory::init($plugin'cron'$job);
                          }
                          break;

                      default:
                          
          // instead of a time-job you can place a worker to execute it directly
                          
          ModelFactory::init($jobs[$job], 'cron'$job);
                          break;
                  }
              }

              
          // der $cmd sollte natürlich keine Zeit-periode ausführen, so das oben im Konstruktor die switch() im default endet.
              
          public static function exe($cmd){
                  
          shell_exec($cmd.' > /dev/null &'); // an den auszuführenden Befehl die Parameter für die Ausführung im Hintergrund (keine Rückmeldung) anhängen
              
          }

          Der Grundgedanke war, erst einmal eine Struktur zu schaffen, in der unterschiedliche Aufgaben angesteuert werden.
          Hier werden dann alle Plugins aufgelistet die Aufgaben in den zeitlichen Kategorien erledigen (wobei jedes unabhängig aussetzen und entscheiden kann).
          Atm, bekommt jedes Plugin ein cron.model.php die dann weiter entscheidet.

          auction/models/cron.model.php
          PHP-Code:
          class auction_cronModel{
              function 
          __construct($task){
                  switch( 
          $task ){
                      case 
          'minit':
                          
          // hier dann die IF-Abfrage die prüft ob der letzte Prozess beendet ist, wenn ja eine neue Liste anlegen und auf die Worker verteilen
                          
          cronjob::exe('php /var/www/vhosts/domain.de/cronjob.php task:aucWorker');
                          
          cronjob::exe('php /var/www/vhosts/domain.de/cronjob.php task:aucWorker2');
                          break;

                      case 
          'aucWorker':
                          
          $this->auction();
                          break;

                      case 
          'aucWorker2':
                          
          $this->auction2();
                          break;
                  }
              }

              private function 
          auction(){
                  global 
          $db;
                  
          sleep(5);
                  
          $db->insert('0_test', ['txt' => 'work at auctions 1']);
              }
              private function 
          auction2(){
                  global 
          $db;
                  
          $db->insert('0_test', ['txt' => 'work at auctions 2']);
              }

          Da ein Plugin ggf. mehr als eine zeitliche Aufgabe erledigen muss, wird im __constructor noch mal geschaut was gerade ausgeführt wird, hier kommt dann auch die eigene entscheidung ins Spiel, wie ihr oben schon erwähntet, kann das Skript ja die Datenbank nutzen um doppelte Ausführungen zu blockieren.

          Da PHP ja so keine Funktion besitzt async zu laufen, habe ich eben die cronjob::exe Funktion hinzugebaut und getestet (funktioniert).
          Denn mein Plan sieht jetzt so aus:
          1. bei Ausführung von 'minit' prüfen ob es gesperrt ist
          2. Wenn nicht, Sperre erstellen und alle zu verarbeitenden Datensätze(nur ID`s) selektieren
          3. Datensätze in einer passenden Anzahl an "Worker" aufteilen (#1: 1-10, #2 :11-20, #2: 21-30 (Datensätze), usw.)
          4. Wenn die Liste erstellt ist den jeweiligen Worker starten cronjob::exe('php /var/www/vhosts/domain.de/cronjob.php task:aucWorker');
          5. Datensätze als "verarbeitet" in der DB speichern (damit sie nicht im nächsten Durchlauf erfasst werden)
          6. Sperre wieder aufheben
          So sollte es nicht nur keine doppelten Verarbeitungen mehr geben ich kann sogar die Datensätze aufteilen und simultan abarbeiten lassen (Jeder Worker weiß ja exakt welche ID`s er zu verarbeiten hat).
          Noch etwas schönes, der Webspace selbst benötigt dazu nicht die Rechte um shell ausführen zu dürfen (nutze Plesk und müsste eig. unterbunden sein) denn PHP CLI hat ja seine eigene php.ini in der man shell für die cronjobs dann zulassen kann.


          Was meint ihr, ist es eine gute Lösung?
          Da ich kein Server-experte bin, kann mich der Befehl " > /dev/null &" später behindern? Das Prozess nicht beendet werden oder Server überlasten? Welche Gefahren gibt es?

          Gibt es eine Begrenzung der Zeichenanzahl bei shell_exec() bzw. CLI ? Wenn nicht, könnte ich mir ja ggf. die ID`s direkt als Parameter übergeben und spare mir ein DB-Handling.


          MfG: Paykoman

          Kommentar


          • #6
            Auf Linux-Systemen reicht es per ps die Anzahl der Tasks des betreffenden Skriptes zu ermitteln. Die Auswertung des ps-Commands ist jedoch nicht trivial, wenn es nicht nur für ein spezielles System gelten soll.
            Für Einsteiger aus meiner Sicht daher nicht zu empfehlen.

            Edit: Betrifft #4

            Kommentar


            • #7
              Warum muss es denn ein Cronjob sein? Wenn du das 1-Minute Limit eh nicht halten kannst, dann benutzt doch lieber eine Message Queue wie z.B. RabbitMQ die einfach kontinuierlich deine Jobs abarbeitet. Da kannst du auch ein Offset von 60 Sekunden setzen, wenn du vor dem Versand unbedingt eine Pause brauchst.
              [I]You know, my wife sometimes looks at me strangely. „Duncan“, she says, „there's more to life than Solaris“. Frankly, it's like she speaks another language. I mean, the words make sense individually, but put them together and it's complete nonsense.[/I]

              Kommentar


              • #8
                Naja weil ein Cronjob wenig Einarbeitungszeit benötigt, bin gestern auch schon auf runwhere gestoßen aber mir scheint das ich da einfach mehr Zeit brauche um mich mit neuen System auseinander zusetzten (die ich nicht habe).
                RabbitMQ schaut auf den ersten Blick auch nicht mal eben so zu sein, und den Prozess einfach in die Länge ziehen oder mit dem offset, weiß nicht ob sich die Probleme da lösen (letztlich muss es ja auch zeitnah passieren).

                Die Frage ist, warum denn nicht so wie oben angedacht? Was spricht dagegen?
                Ich finde so ist es doch eigentlich sehr gut gelöst, man muss natürlich sorgfältig damit umgehen sonst läuft php ggf. im dauer loop (wie gerade eben LOL)

                Wenn man das asynchrone abarbeiten auf die Spitze treiben möchte (muss) dann könnte man das mit dem exe sogar zusätzlich noch eine Etappe vorher machen, so das jedes Plugin im eigenen Prozess läuft dann würden diese nicht mehr der reihe nach im selben Cronjob laufen.

                Gut es gibt keine Rückmeldung am Ende, finde das aber nicht schlimm, ob alles so läuft wie es soll, kann ja während der Abarbeitung geprüft werden.

                Wichtig finde ich halt die Fragen die am Ende des Beitrages stehen...

                Kommentar


                • #9
                  • Script startet
                  • ID's der zu bearbeitenden Datensätze selektieren (*)
                  • Verarbeitungspool anlegen
                    • Selektierte ID's diesem Pool zuordnen
                  • Verarbeitung starten und ID's in der Datensatz-Tabelle entsprechend als "fertig" markieren
                  • Verarbeitspool nach Abarbeitung löschen
                  • Nächstes Script startet
                  • ID's der zu bearbeitenden Datensätze selektieren (*)
                  (*) + "WHERE myBlablas.blablaId NOT IN (SELECT blablaId FROM taskPool)" - Alle Datensätze selektieren die verarbeitet werden müssen UND noch nicht in einem der Verarbeitungspools stecken.

                  #5 liest sich ziemlich umständlich (und wirft auch andere Themen mit rein?). TBH hab ich auch keine Zeit mich so sehr in diese spezifische Aufgabenstellung einzulesen, aber wenn es nur darum geht regelmäßig neue Verarbeitungsdurchläufe zu starten die sich nicht überschneiden sollen -> ist eigentlich nicht so schwierig.
                  [COLOR=#A9A9A9]Relax, you're doing fine.[/COLOR]
                  [URL="http://php.net/"]RTFM[/URL] | [URL="http://php-de.github.io/"]php.de Wissenssammlung[/URL] | [URL="http://use-the-index-luke.com/de"]Datenbankindizes[/URL] | [URL="https://www.php.de/forum/webentwicklung/datenbanken/111631-bild-aus-datenbank-auslesen?p=1209079#post1209079"]Dateien in der DB?[/URL]

                  Kommentar


                  • #10
                    Zitat von jspit Beitrag anzeigen
                    Auf Linux-Systemen reicht es per ps die Anzahl der Tasks des betreffenden Skriptes zu ermitteln.

                    Edit: Betrifft #4
                    Stimmt,
                    deswegen hätte ich auch zu
                    Code:
                    #cat /proc/PID/status
                    geraten
                    .
                    Zum Thema:
                    Gut es gibt keine Rückmeldung am Ende, finde das aber nicht schlimm.
                    Ausgaben für cron kann man configurieren.

                    Kommentar


                    • #11
                      Ich finde jetzt deine Klassen nicht gerade so toll, ist aber meine Ansicht.

                      - global $db. Gib es der Klasse mit.

                      - die Klassen sind im Allgemeinen nicht gerade sehr flexibel.

                      - so wie es aussieht, verwendest du kein Autoloader für die Klassen.

                      - kein Typehinting

                      Kommentar


                      • #12
                        @strub: Danke aber mir ging es jetzt weniger ums Design-pattern als um den Umgang mit Cronjobs und der Lösung um genannter Probleme.

                        Trotzdem danke für das Feedback, irgend wann mal wenn ich es mir leisten kann werde ich sicher ein neues Design-pattern anstreben erstmal steht die Funktionalität im Vordergrund.

                        Kommentar


                        • #13
                          Trotzdem danke für das Feedback, irgend wann mal wenn ich es mir leisten kann werde ich sicher ein neues Design-pattern anstreben erstmal steht die Funktionalität im Vordergrund.
                          Das ist der falsche weg, glaube mir ich machte den selben Fehler und schrieb die ganzen Klassen jetzt neu. Daher mein Tip: Mache es von Anfang an richtig.
                          Damit meine ich mache die Klassen so flexibel wie möglich. Benutze Interfaces als Beispiel.

                          Hier mal ein kleine (flexibles) design, wie ich es gemacht habe.

                          PHP-Code:
                          $schedule = new Schedule();

                          // add task
                          $schedule->task(TaskInterface $task)->at(['17:00']);

                          // another task
                          $schedule->call(function() {
                              
                          // do sth.
                          })
                          ->
                          description('Any description');
                          ->
                          before(function () {
                              
                          // Task is about to start...
                          })
                          ->
                          after(function () {
                               
                          // Task is complete...
                          });
                          ->
                          days(['mo''fr'])
                          ->
                          hourly('22');



                          $processor = new ScheduleProcessor(ScheduleInterface $scheduleContainerInterface $container);
                          $processor->setTimeout(120);

                          $processor->before(function($processor)
                          {
                              
                          // log, update, delete tasks from database e.g.
                          });

                          $processor->after(function($processor)
                          {
                              
                          // log, update, delete tasks from database e.g.
                          });

                          $processor->onFailure(function($processor$throwable)
                          {
                              
                          // do something
                          });

                          $processor->run(); 

                          Kommentar


                          • #14
                            Ich werde mir dein Beispiel mal abspeichern, danke.

                            Kommentar

                            Lädt...
                            X