Exemple #1
0
 /**
  * This is called after setup() returns
  * @return void
  */
 public function start()
 {
     // This is just going to sleep a really long time.
     // I'll replace this with a better demo in a future version.
     // The idea is that the easiest way to parallelize some code in your daemon is to pass a closure or callback to the task() method.
     // But if you have a complex task that can get ugly and difficult to read and understand. In those cases, you can implement
     // a Core_ITask object like this one.
     $this->daemon->log("Starting BigTask...");
     sleep($this->sleep_duration);
     if ($this->wakeup_message) {
         $this->daemon->log($this->wakeup_message);
     }
 }
 /**
  * Maintain the worker process map and notify the worker of an exited process.
  * @param bool $block   When true, method will block waiting for an exit signal
  * @return void
  */
 public function reap($block = false)
 {
     $map = $this->processes();
     while (true) {
         $pid = pcntl_wait($status, $block === true && $this->daemon->is('parent') ? NULL : WNOHANG);
         if (!$pid || !isset($map[$pid])) {
             break;
         }
         $alias = $map[$pid]->group;
         $process = $this->processes[$alias][$pid];
         $this->daemon->dispatch(array(Core_Daemon::ON_REAP), array($process, $status));
         unset($this->processes[$alias][$pid]);
         // Keep track of process churn -- failures within a processes min_ttl
         // If too many failures of new processes occur inside a given interval, that's a problem.
         // Raise a fatal error to prevent runaway process forking which can be very damaging to a server
         if ($this->daemon->is('shutdown') || $process->runtime() >= $process->min_ttl) {
             continue;
         }
         foreach ($this->failures as $key => $failure_time) {
             if ($failure_time + self::CHURN_WINDOW < time()) {
                 unset($this->failures[$key]);
             }
         }
         if (count($this->failures) > self::CHURN_LIMIT) {
             $this->daemon->fatal_error("Recently forked processes are continuously failing. See error log for additional details.");
         }
     }
 }
Exemple #3
0
 /**
  * Start the engines own loop
  *
  * @return void
  */
 public function doWork()
 {
     $this->mediator->log('Sunrise is working');
     while (!\Core_Daemon::is('shutdown')) {
         usleep(1000);
     }
 }
Exemple #4
0
 /**
  * serializes the build before shutting down
  *
  * @return void
  */
 public function tearDown()
 {
     if (!\Core_Daemon::is('parent')) {
         $this->mediator->log('TearDown');
         if ($this->build != null) {
             $this->serializeBuild($this->build);
             $this->build = null;
         }
     }
 }
Exemple #5
0
 /**
  * This is called after setup() returns
  * @return void
  */
 public function start()
 {
     $post = $this->post;
     // Send to Twitter:
     $tmhOAuth = new \tmhOAuth(array());
     $code = $tmhOAuth->request('POST', $tmhOAuth->url('1/statuses/update'), array('status' => $post['content']));
     // There is no special handling of API errors.
     // Right now we just dump the response to MongoDB
     $post['code'] = $code;
     $post['response'] = json_decode($tmhOAuth->response['response'], true);
     // Move this post to another collection named archive:
     unset($post['processing']);
     unset($post['processing_time']);
     $m = new \Mongo();
     $m->tampon->archive->insert($post);
     $m->tampon->posts->remove(array('_id' => $post['_id']));
     if ($code == 200) {
         $this->daemon->log(sprintf("Sent post %s to Twitter, Twitter id: %s by user %s", $post['_id'], (string) $post['response']['id'], $post['response']['screen_name']));
     } else {
         $this->daemon->log(sprintf("Failed sending post %s to Twitter, error code %s: %s", $post['_id'], (string) $code, $post['response']['error']), "warning");
     }
 }
Exemple #6
0
 /**
  * Dispatch ON_ERROR event, write an error message to the event log, and restart the worker.
  *
  * Part of the Worker API - Use from your worker to log a fatal error message and restart the current process.
  *
  * @param $message
  * @return void
  */
 public function fatal_error($message)
 {
     if ($this->daemon->is('parent')) {
         $this->daemon->fatal_error("Fatal Error: {$message}", $this->alias);
     } else {
         $this->daemon->fatal_error("Fatal Error: {$message}\nWorker process will restart", $this->alias);
     }
 }
Exemple #7
0
 private function log($message)
 {
     $this->daemon->log($message, 'SocketServer');
 }
Exemple #8
0
 /**
  * Log the $message to the filename returned by Core_Daemon::log_file() and/or optionally print to stdout.
  * Multi-Line messages will be handled nicely.
  *
  * Note: Your log_file() method will be called every 5 minutes (at even increments, eg 00:05, 00:10, 00:15, etc) to
  * allow you to rotate the filename based on time (one log file per month, day, hour, whatever) if you wish.
  *
  * Note: You may find value in overloading this method in your app in favor of a more fully-featured logging tool
  * like log4php or Zend_Log. There are fantastic logging libraries available, and this simplistic home-grown option
  * was chosen specifically to avoid forcing another dependency on you.
  *
  * @param string $message
  * @param string $label Truncated at 12 chars
  */
 public function log($message, $label = '', $indent = 0)
 {
     static $log_file = '';
     static $log_file_check_at = 0;
     static $log_file_error = false;
     $header = "\nDate                  PID   Label         Message\n";
     $date = date("Y-m-d H:i:s");
     $pid = str_pad($this->pid, 5, " ", STR_PAD_LEFT);
     $label = str_pad(substr($label, 0, 12), 13, " ", STR_PAD_RIGHT);
     $prefix = "[{$date}] {$pid} {$label}" . str_repeat("\t", $indent);
     if (time() >= $log_file_check_at && $this->log_file() != $log_file) {
         $log_file = $this->log_file();
         $log_file_check_at = mktime(date('H'), date('i') - date('i') % 5 + 5, null);
         @fclose(self::$log_handle);
         self::$log_handle = $log_file_error = false;
     }
     if (self::$log_handle === false) {
         if (strlen($log_file) > 0 && (self::$log_handle = @fopen($log_file, 'a+'))) {
             if ($this->is('parent')) {
                 fwrite(self::$log_handle, $header);
                 if ($this->is('stdout')) {
                     echo $header;
                 }
             }
         } elseif (!$log_file_error) {
             $log_file_error = true;
             trigger_error(__CLASS__ . "Error: Could not write to logfile " . $log_file, E_USER_WARNING);
         }
     }
     $message = $prefix . ' ' . str_replace("\n", "\n{$prefix} ", trim($message)) . "\n";
     if (self::$log_handle) {
         fwrite(self::$log_handle, $message);
     }
     if ($this->is('stdout')) {
         echo $message;
     }
 }
Exemple #9
0
 /**
  * Display a command prompt, block on input from STDIN, then parse and execute the specified commands.
  *
  * Multiple processes share a single command prompt by accessing a semaphore identified by the current application.
  * This method will block the process while it waits for the mutex, and then again while it waits for input on STDIN.
  *
  * The text of the prompt itself will be written when get_text_prompt() is called. Custom prompts for a given $method
  * can be added to the $prompts array.
  *
  * Several commands are built-in, and additional commands can be added with addParser().
  *
  * Parsers can either:
  * 1. Continue from the prompt.
  * 2. Abort from the prompt. Call any interrupt_callable that may be registered for this $method.
  * 3. Take some action or perform some activity and then return to the same prompt for additional commands.
  *
  * @param $method
  * @param $args
  * @return bool|int|mixed|null
  * @throws Exception
  */
 public function prompt($method, $args)
 {
     if (!is_resource($this->shm)) {
         return true;
     }
     // The single debug shell is shared across the parent and all worker processes. Use a mutex to serialize
     // access to the shell. If the mutex isn't owned by this process, this will block until this process acquires it.
     $this->mutex_acquire();
     if (!$this->is_breakpoint_active($method)) {
         $this->mutex_release();
         return true;
     }
     // Pass a simple print-line closure to parsers to use instead of just "echo" or "print"
     $printer = function ($message, $maxlen = null) {
         if (empty($message)) {
             return;
         }
         if ($maxlen && strlen($message) > $maxlen) {
             $message = substr($message, 0, $maxlen - 3) . '...';
         }
         $message = str_replace(PHP_EOL, PHP_EOL . ' ', $message);
         echo " {$message}\n\n";
     };
     try {
         $this->print_banner();
         $pid = getmypid();
         $prompt = $this->get_text_prompt($method, $args);
         $break = false;
         // We have to clear the buffer of any input that occurred in the terminal in the space after they submitted their last
         // command and before this new prompt. Otherwise it'll be read from fgets below and probably ruin everything.
         stream_set_blocking(STDIN, 0);
         while (fgets(STDIN)) {
             continue;
         }
         stream_set_blocking(STDIN, 1);
         // Commands that set $break=true will continue forward from the command prompt.
         // Otherwise it will just do the action (or display an error) and then repeat the prompt
         while (!$break) {
             echo $prompt;
             $input = trim(fgets(STDIN));
             $input = preg_replace('/\\s+/', ' ', $input);
             $matches = false;
             $message = '';
             // Use the familiar bash !! to re-run the last command
             if (substr($input, -2) == '!!') {
                 $input = $this->debug_state('last');
             } elseif (!empty($input)) {
                 $this->debug_state('last', $input);
             }
             // Validate the input as an expression
             $matches = array();
             foreach ($this->parsers as $parser) {
                 if (preg_match($parser['regex'], $input, $matches) == 1) {
                     $break = $parser['closure']($matches, $printer);
                     break;
                 }
             }
             if ($matches) {
                 continue;
             }
             // If one of the parsers didn't catch the message
             // fall through to the built-in commands
             switch (strtolower($input)) {
                 case 'help':
                     $out = array();
                     $out[] = 'For the PHP Simple Daemon debugging guide, see: ';
                     $out[] = 'https://github.com/shaneharter/PHP-Daemon/wiki/Debugging-Workers';
                     $out[] = '';
                     $out[] = 'Available Commands:';
                     $out[] = 'y                 Step to the next break point';
                     $out[] = 'n                 Interrupt';
                     $out[] = '';
                     $out[] = 'capture           Call the current method and capture its return value. Will print_r the return value and return a prompt.';
                     $out[] = 'end               End the debugging session, continue the daemon as normal.';
                     $out[] = 'help              Print This Help';
                     $out[] = 'kill              Kill the daemon and all of its worker processes.';
                     $out[] = 'skip              Skip this breakpoint from now on.';
                     $out[] = 'shutdown          End Debugging and Gracefully shutdown the daemon after the current loop_interval.';
                     $out[] = 'trace             Print A Stack Trace';
                     if (is_callable($this->indent_callback)) {
                         $out[] = 'indent [y|n]      When turned-on, indentation will be used to group messages from the same call in a column so you can easily match them together.';
                     }
                     $out[] = '';
                     foreach ($this->parsers as $parser) {
                         $out[] = sprintf('%s%s', str_pad($parser['command'], 18, ' ', STR_PAD_RIGHT), $parser['description']);
                     }
                     $out[] = '';
                     $out[] = '!!                Repeat previous command';
                     $printer(implode(PHP_EOL, $out));
                     break;
                 case 'indent y':
                     $this->debug_state('indent', true);
                     $printer('Indent enabled');
                     break;
                 case 'indent n':
                     $this->debug_state('indent', false);
                     $printer('Indent disabled');
                     break;
                 case 'show args':
                     $printer(print_r($args, true));
                     break;
                 case 'shutdown':
                     //$this->daemon->shutdown();
                     $printer("Shutdown In Progress... Use `end` command to cease debugging until shutdown is complete.");
                     $break = true;
                     break;
                 case 'trace':
                     $e = new exception();
                     $printer($e->getTraceAsString());
                     break;
                 case 'end':
                     $this->debug_state('enabled', false);
                     $break = true;
                     $printer('Debugging Ended..');
                     $input = true;
                     break;
                 case 'skip':
                     $this->debug_state("skip_{$method}", true);
                     $printer('Breakpoint "' . $method . '" Turned Off..');
                     $break = true;
                     $input = true;
                     break;
                 case 'kill':
                     @fclose(STDOUT);
                     @fclose(STDERR);
                     @exec('ps -C "php ' . Core_Daemon::get('filename') . '" -o pid= | xargs kill -9 ');
                     break;
                 case 'capture':
                     $backtrace = debug_backtrace();
                     if ($backtrace[1]['function'] !== '__call' || $method == self::CAPTURE) {
                         $printer('Cannot capture this :(');
                         break;
                     }
                     $input = self::CAPTURE;
                     $break = true;
                     break;
                 case 'y':
                     $input = self::CONT;
                     $break = true;
                     break;
                 case 'n':
                     $input = self::ABORT;
                     $break = true;
                     break;
                 default:
                     if ($input) {
                         $printer("Unknown Command! See `help` for list of commands.");
                     }
             }
         }
     } catch (Exception $e) {
         $this->mutex_release();
         throw $e;
     }
     $this->mutex_release();
     return $input;
 }
Exemple #10
0
 public function setup()
 {
     $ftok = ftok(Core_Daemon::get('filename'), 'L');
     $this->shm = shm_attach($ftok, 512, 0666);
 }
Exemple #11
0
 /**
  * Handle IPC Errors
  * @param $error
  * @param int $try    Inform error() of repeated failures of the same $error_code
  * @return boolean  Returns true if the operation should be retried.
  */
 public function error($error, $try = 1)
 {
     // Create an array of random, moderate size and verify it can be written to shared memory
     // Return boolean
     $that = $this;
     $test = function () use($that) {
         $arr = array_fill(0, mt_rand(10, 100), mt_rand(1000, 1000 * 1000));
         $key = mt_rand(1000 * 1000, 2000 * 1000);
         @shm_put_var($that->shm, $key, $arr);
         usleep(5000);
         return @shm_get_var($that->shm, $key) == $arr;
     };
     switch ($error) {
         case 0:
             // Success
         // Success
         case 4:
             // System Interrupt
         // System Interrupt
         case MSG_ENOMSG:
             // No message of desired type
             // Ignored Errors
             return true;
             break;
         case MSG_EAGAIN:
             // Temporary Problem, Try Again
             usleep($this->mediator->backoff(20000, $try));
             return true;
             break;
         case 13:
             // Permission Denied
             $this->mediator->count_error('communication');
             $this->mediator->log('Permission Denied: Cannot connect to message queue');
             $this->purge_mq();
             if (Core_Daemon::is('parent')) {
                 usleep($this->mediator->backoff(100000, $try));
             } else {
                 sleep($this->mediator->backoff(3, $try));
             }
             $this->setup_ipc();
             return true;
             break;
         case 22:
             // Invalid Argument
             // Probably because the queue was removed in another process.
         // Invalid Argument
         // Probably because the queue was removed in another process.
         case 43:
             // Identifier Removed
             // A message queue was re-created at this address but the resource identifier we have needs to be re-created
             $this->mediator->count_error('communication');
             if (Core_Daemon::is('parent')) {
                 usleep($this->mediator->backoff(20000, $try));
             } else {
                 sleep($this->mediator->backoff(2, $try));
             }
             $this->setup_ipc();
             return true;
             break;
         case self::ERROR_UNKNOWN:
             // Almost certainly an issue with shared memory
             $this->mediator->log("Shared Memory I/O Error at Address {$this->mediator->guid}.");
             $this->mediator->count_error('corruption');
             // If this is a worker, all we can do is try to re-attach the shared memory.
             // Any corruption or OOM errors will be handled by the parent exclusively.
             if (!Core_Daemon::is('parent')) {
                 sleep($this->mediator->backoff(3, $try));
                 $this->setup_ipc();
                 return true;
             }
             // If this is the parent, do some diagnostic checks and attempt correction.
             usleep($this->mediator->backoff(20000, $try));
             // Test writing to shared memory using an array that should come to a few kilobytes.
             for ($i = 0; $i < 2; $i++) {
                 if ($test()) {
                     return true;
                 }
                 // Re-attach the shared memory and try the diagnostic again
                 $this->setup_ipc();
             }
             $this->mediator->log("IPC DIAG: Re-Connect failed to solve the problem.");
             if (!$this->mediator->daemon->is('parent')) {
                 break;
             }
             // Attempt to re-connect the shared memory
             // See if we can read what's in shared memory and re-write it later
             $items_to_copy = array();
             $items_to_call = array();
             for ($i = 0; $i < $this->mediator->call_count; $i++) {
                 $call = @shm_get_var($this->shm, $i);
                 if (!is_object($call)) {
                     continue;
                 }
                 $cached = $this->mediator->get_struct($i);
                 if (!is_object($cached)) {
                     continue;
                 }
                 if ($cached->status == Core_Worker_Mediator::TIMEOUT) {
                     continue;
                 }
                 if ($cached->status == Core_Worker_Mediator::UNCALLED) {
                     $items_to_call[$i] = $call;
                     continue;
                 }
                 $items_to_copy[$i] = $call;
             }
             $this->mediator->log("IPC DIAG: Preparing to clean SHM and Reconnect...");
             for ($i = 0; $i < 2; $i++) {
                 $this->purge_shm();
                 $this->setup_ipc();
                 if (!empty($items_to_copy)) {
                     foreach ($items_to_copy as $key => $value) {
                         @shm_put_var($this->shm, $key, $value);
                     }
                 }
                 if (!$test()) {
                     if (empty($items_to_copy)) {
                         $this->mediator->fatal_error("Shared Memory Failure: Unable to proceed.");
                     } else {
                         $this->mediator->log('IPC DIAG: Purging items from shared memory: ' . implode(', ', array_keys($items_to_copy)));
                         unset($items_to_copy);
                     }
                 }
             }
             foreach ($items_to_call as $call) {
                 $this->mediator->retry($call);
             }
             return true;
         default:
             if ($error) {
                 $this->mediator->log("Message Queue Error {$error}: " . posix_strerror($error));
             }
             if (Core_Daemon::is('parent')) {
                 usleep($this->mediator->backoff(100000, $try));
             } else {
                 sleep($this->mediator->backoff(3, $try));
             }
             $this->mediator->count_error('catchall');
             $this->setup_ipc();
             return false;
     }
 }