From 0e6a6c3d26d42c3f41d75b6b8a4255d5cbf2a300 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Sat, 8 Feb 2014 17:28:01 -0800 Subject: [PATCH] Renaming BackoffPlugin to RetryPlugin and fixing MessageFormatter RetryPlugin now supports logging retries by intercepting the retry delay function. Updated the MessageFormatter to properly log request and response strings. --- src/Guzzle/Plugin/Backoff/composer.json | 28 ----- src/Guzzle/Plugin/Log/MessageFormatter.php | 78 ++++++------- .../RetryPlugin.php} | 106 ++++++++++++++---- 3 files changed, 118 insertions(+), 94 deletions(-) delete mode 100644 src/Guzzle/Plugin/Backoff/composer.json rename src/Guzzle/Plugin/{Backoff/BackoffPlugin.php => Retry/RetryPlugin.php} (55%) diff --git a/src/Guzzle/Plugin/Backoff/composer.json b/src/Guzzle/Plugin/Backoff/composer.json deleted file mode 100644 index 91c122cb..00000000 --- a/src/Guzzle/Plugin/Backoff/composer.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "guzzle/plugin-backoff", - "description": "Guzzle backoff retry plugins", - "homepage": "http://guzzlephp.org/", - "keywords": ["plugin", "guzzle"], - "license": "MIT", - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "require": { - "php": ">=5.3.2", - "guzzle/http": "self.version", - "guzzle/log": "self.version" - }, - "autoload": { - "psr-0": { "Guzzle\\Plugin\\Backoff": "" } - }, - "target-dir": "Guzzle/Plugin/Backoff", - "extra": { - "branch-alias": { - "dev-master": "3.7-dev" - } - } -} diff --git a/src/Guzzle/Plugin/Log/MessageFormatter.php b/src/Guzzle/Plugin/Log/MessageFormatter.php index 683dba2a..afc4884f 100644 --- a/src/Guzzle/Plugin/Log/MessageFormatter.php +++ b/src/Guzzle/Plugin/Log/MessageFormatter.php @@ -6,30 +6,30 @@ use Guzzle\Http\Message\RequestInterface; use Guzzle\Http\Message\ResponseInterface; /** - * Message formatter used in various places in the framework + * Formats messages using variable substitutions for requests, responses, and other transactional data. * - * Format messages using a template that can contain the the following variables: + * The following variable substitutions are supported: * - * - {request}: Full HTTP request message - * - {response}: Full HTTP response message - * - {ts}: Timestamp - * - {host}: Host of the request - * - {method}: Method of the request - * - {url}: URL of the request - * - {host}: Host of the request - * - {protocol}: Request protocol - * - {version}: Protocol version - * - {resource}: Resource of the request (path + query + fragment) - * - {hostname}: Hostname of the machine that sent the request - * - {code}: Status code of the response (if available) - * - {phrase}: Reason phrase of the response (if available) - * - {error}: Any error messages (if available) - * - {req_header_*}: Replace `*` with the lowercased name of a request header to add to the message - * - {res_header_*}: Replace `*` with the lowercased name of a response header to add to the message - * - {req_headers}: Request headers - * - {res_headers}: Response headers - * - {req_body}: Request body - * - {res_body}: Response body + * - {request}: Full HTTP request message + * - {response}: Full HTTP response message + * - {ts}: Timestamp + * - {host}: Host of the request + * - {method}: Method of the request + * - {url}: URL of the request + * - {host}: Host of the request + * - {protocol}: Request protocol + * - {version}: Protocol version + * - {resource}: Resource of the request (path + query + fragment) + * - {hostname}: Hostname of the machine that sent the request + * - {code}: Status code of the response (if available) + * - {phrase}: Reason phrase of the response (if available) + * - {error}: Any error messages (if available) + * - {req_header_*}: Replace `*` with the lowercased name of a request header to add to the message + * - {res_header_*}: Replace `*` with the lowercased name of a response header to add to the message + * - {req_headers}: Request headers + * - {res_headers}: Response headers + * - {req_body}: Request body + * - {res_body}: Response body */ class MessageFormatter { @@ -38,7 +38,7 @@ class MessageFormatter const SHORT_FORMAT = '[{ts}] "{method} {resource} {protocol}/{version}" {code}'; /** @var string Template used to format log messages */ - protected $template; + private $template; /** * @param string $template Log message template @@ -48,20 +48,6 @@ class MessageFormatter $this->template = $template ?: self::DEFAULT_FORMAT; } - /** - * Set the template to use for logging - * - * @param string $template Log message template - * - * @return self - */ - public function setTemplate($template) - { - $this->template = $template; - - return $this; - } - /** * Returns a formatted message * @@ -97,10 +83,18 @@ class MessageFormatter $result = $response; break; case 'req_headers': - $result = $request->getStartLine() . "\r\n" . $request->getHeaders(); + $result = trim($request->getMethod() . ' ' + . $request->getResource()) . ' HTTP/' + . $request->getProtocolVersion() . "\r\n" + . $request->getHeaders(); break; case 'res_headers': - $result = $response->getStartLine() . "\r\n" . $response->getHeaders(); + $result = sprintf( + 'HTTP/%s %d %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ) . "\r\n" . $response->getHeaders(); break; case 'req_body': $result = $request->getBody(); @@ -133,13 +127,13 @@ class MessageFormatter $result = gethostname(); break; case 'code': - $result = $response ? $response->getStatusCode() : ''; + $result = $response ? $response->getStatusCode() : 'NULL'; break; case 'phrase': - $result = $response ? $response->getReasonPhrase() : ''; + $result = $response ? $response->getReasonPhrase() : 'NULL'; break; case 'error': - $result = $error ? $error->getMessage() : null; + $result = $error ? $error->getMessage() : 'NULL'; break; default: if (strpos($matches[1], 'req_header_') === 0) { diff --git a/src/Guzzle/Plugin/Backoff/BackoffPlugin.php b/src/Guzzle/Plugin/Retry/RetryPlugin.php similarity index 55% rename from src/Guzzle/Plugin/Backoff/BackoffPlugin.php rename to src/Guzzle/Plugin/Retry/RetryPlugin.php index 62ffeef3..71a26323 100644 --- a/src/Guzzle/Plugin/Backoff/BackoffPlugin.php +++ b/src/Guzzle/Plugin/Retry/RetryPlugin.php @@ -1,16 +1,23 @@ ['onRequestSent'], + RequestEvents::ERROR => ['onRequestSent'] + ]; + } + /** - * @param callable $filter Filter used to determine whether or not to retry a request - * @param callable $delayFunc Callable that accepts the number of retries and returns the amount of - * of time in seconds to delay. + * @param callable $filter Filter used to determine whether or not to retry a request. The filter must be a + * callable that accepts the current number of retries and an AbstractTransferStatsEvent + * object. The filter must return true or false to denote if the request must be retried. + * @param callable $delayFunc Callable that accepts the number of retries and an AbstractTransferStatsEvent and + * returns the amount of of time in seconds to delay. * @param int $maxRetries Maximum number of retries */ public function __construct( @@ -39,35 +56,43 @@ class BackoffPlugin implements EventSubscriberInterface /** * Retrieve a basic truncated exponential backoff plugin that will retry HTTP errors and cURL errors * - * @param int $maxRetries Maximum number of retries - * @param array $httpCodes HTTP response codes to retry - * @param array $curlCodes cURL error codes to retry + * @param int $maxRetries Maximum number of retries + * @param array $httpCodes HTTP response codes to retry + * @param array $curlCodes cURL error codes to retry + * @param LoggerInterface|bool $logger Pass a logger instance to log each retry or pass true for STDOUT + * @param string|MessageFormatter $formatter Pass a message formatter if logging to customize log messages * * @return self + * @throws \InvalidArgumentException if logger is not a boolean or LoggerInterface */ public static function getExponentialBackoff( $maxRetries = 3, array $httpCodes = null, - array $curlCodes = null + array $curlCodes = null, + $logger = null, + $formatter = null ) { - if (extension_loaded('curl')) { + if (!extension_loaded('curl')) { + $filter = self::createStatusFilter($httpCodes); + } else { $filter = self::createChainFilter([ self::createStatusFilter($httpCodes), self::createCurlFilter($curlCodes) ]); - } else { - $filter = self::createStatusFilter($httpCodes); } - return new self($filter, array(__CLASS__, 'exponentialDelay'), $maxRetries); - } + $delay = [__CLASS__, 'exponentialDelay']; - public static function getSubscribedEvents() - { - return [ - RequestEvents::AFTER_SEND => ['onRequestSent'], - RequestEvents::ERROR => ['onRequestSent'] - ]; + if ($logger) { + if ($logger === true) { + $logger = new SimpleLogger(STDOUT); + } elseif (!($logger instanceof LoggerInterface)) { + throw new \InvalidArgumentException('$logger must be true, false, or a LoggerInterface'); + } + $delay = self::createLoggingDelay($delay, $logger, $formatter); + } + + return new self($filter, $delay, $maxRetries); } public function onRequestSent(AbstractTransferStatsEvent $event) @@ -79,7 +104,7 @@ class BackoffPlugin implements EventSubscriberInterface $filterFn = $this->filter; if ($filterFn($retries, $event)) { $delayFn = $this->delayFunc; - sleep($delayFn($retries)); + sleep($delayFn($retries, $event)); $request->getConfig()->set('retries', ++$retries); $event->intercept($event->getClient()->send($request)); } @@ -89,15 +114,48 @@ class BackoffPlugin implements EventSubscriberInterface /** * Returns an exponential delay calculation * - * @param int $retries Number of retries so far + * @param int $retries Number of retries so far + * @param AbstractTransferStatsEvent $event Event containing transactional information * * @return int */ - public static function exponentialDelay($retries) + public static function exponentialDelay($retries, AbstractTransferStatsEvent $event) { return (int) pow(2, $retries - 1); } + /** + * Creates a delay function that logs each retry before proxying to a wrapped delay function. + * + * @param callable $delayFn Delay function to proxy to + * @param LoggerInterface $logger Logger used to log messages + * @param string|MessageFormatter $formatter Format string or Message formatter used to format messages + * + * @return callable + */ + public static function createLoggingDelay( + callable $delayFn, + LoggerInterface $logger, + $formatter = null + ) { + if (!$formatter) { + $formatter = new MessageFormatter(self::RETRY_FORMAT); + } elseif (!($formatter instanceof MessageFormatter)) { + $formatter = new MessageFormatter($formatter); + } + + return function ($retries, AbstractTransferStatsEvent $event) use ($delayFn, $logger, $formatter) { + $delay = $delayFn($retries, $event); + $logger->log(LogLevel::NOTICE, $formatter->format( + $event->getRequest(), + $event->getResponse(), + $event instanceof RequestErrorEvent ? $event->getException() : null, + ['retries' => $retries + 1, 'delay' => $delay] + $event->getTransferInfo() + )); + return $delay; + }; + } + /** * Creates a retry filter based on HTTP status codes *