/** * 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."); } } }
/** * Start the engines own loop * * @return void */ public function doWork() { $this->mediator->log('Sunrise is working'); while (!\Core_Daemon::is('shutdown')) { usleep(1000); } }
/** * 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; } } }
/** * 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"); } }
/** * 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); } }
private function log($message) { $this->daemon->log($message, 'SocketServer'); }
/** * 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; } }
/** * 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; }
public function setup() { $ftok = ftok(Core_Daemon::get('filename'), 'L'); $this->shm = shm_attach($ftok, 512, 0666); }
/** * 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; } }