diff --git a/composer.json b/composer.json index 1df355c..225ad84 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ ], "require": { "composer/installers": "~1.0", - "php-amqplib/php-amqplib": ">=2.6.1" + "php-amqplib/php-amqplib": ">=2.6.1", + "aws/aws-sdk-php": "^3.64" }, "require-dev": { "phpunit/phpunit": "~3.7", diff --git a/composer.lock b/composer.lock index f710ec0..d1e58c4 100644 --- a/composer.lock +++ b/composer.lock @@ -1,23 +1,106 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3da539add4bc42dcecdaa80e0c670adc", + "content-hash": "d3dcdd83f1248fda68e1549370859fd1", "packages": [ + { + "name": "aws/aws-sdk-php", + "version": "3.69.3", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "92ade997fc057d22bbee902468f749ef8db1c162" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/92ade997fc057d22bbee902468f749ef8db1c162", + "reference": "92ade997fc057d22bbee902468f749ef8db1c162", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "guzzlehttp/guzzle": "^5.3.1|^6.2.1", + "guzzlehttp/promises": "~1.0", + "guzzlehttp/psr7": "^1.4.1", + "mtdowling/jmespath.php": "~2.2", + "php": ">=5.5" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "phpunit/phpunit": "^4.8.35|^5.4.3", + "psr/cache": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Aws\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "time": "2018-10-09T20:35:16+00:00" + }, { "name": "composer/installers", - "version": "v1.3.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/composer/installers.git", - "reference": "79ad876c7498c0bbfe7eed065b8651c93bfd6045" + "reference": "cfcca6b1b60bc4974324efb5783c13dca6932b5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/installers/zipball/79ad876c7498c0bbfe7eed065b8651c93bfd6045", - "reference": "79ad876c7498c0bbfe7eed065b8651c93bfd6045", + "url": "https://api.github.com/repos/composer/installers/zipball/cfcca6b1b60bc4974324efb5783c13dca6932b5b", + "reference": "cfcca6b1b60bc4974324efb5783c13dca6932b5b", "shasum": "" }, "require": { @@ -29,7 +112,7 @@ }, "require-dev": { "composer/composer": "1.0.*@dev", - "phpunit/phpunit": "4.1.*" + "phpunit/phpunit": "^4.8.36" }, "type": "composer-plugin", "extra": { @@ -63,6 +146,7 @@ "Hurad", "ImageCMS", "Kanboard", + "Lan Management System", "MODX Evo", "Mautic", "Maya", @@ -86,6 +170,7 @@ "croogo", "dokuwiki", "drupal", + "eZ Platform", "elgg", "expressionengine", "fuelphp", @@ -98,14 +183,18 @@ "lavalite", "lithium", "magento", + "majima", "mako", "mediawiki", "modulework", + "modx", "moodle", + "osclass", "phpbb", "piwik", "ppi", "puppet", + "pxcms", "reindex", "roundcube", "shopware", @@ -118,20 +207,256 @@ "zend", "zikula" ], - "time": "2017-04-24T06:37:16+00:00" + "time": "2018-08-27T06:10:37+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.3-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2018-04-22T15:46:56+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2017-03-20T17:10:46+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac", + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "JmesPath\\": "src/" + }, + "files": [ + "src/JmesPath.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "time": "2016-12-03T22:08:25+00:00" }, { "name": "php-amqplib/php-amqplib", - "version": "v2.6.3", + "version": "v2.7.2", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "fa2f0d4410a11008cb36b379177291be7ee9e4f6" + "reference": "dfd3694a86f1a7394d3693485259d4074a6ec79b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/fa2f0d4410a11008cb36b379177291be7ee9e4f6", - "reference": "fa2f0d4410a11008cb36b379177291be7ee9e4f6", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/dfd3694a86f1a7394d3693485259d4074a6ec79b", + "reference": "dfd3694a86f1a7394d3693485259d4074a6ec79b", "shasum": "" }, "require": { @@ -143,6 +468,7 @@ "videlalvaro/php-amqplib": "self.version" }, "require-dev": { + "phpdocumentor/phpdocumentor": "^2.9", "phpunit/phpunit": "^4.8", "scrutinizer/ocular": "^1.1", "squizlabs/php_codesniffer": "^2.5" @@ -163,7 +489,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1" + "LGPL-2.1-or-later" ], "authors": [ { @@ -188,7 +514,57 @@ "queue", "rabbitmq" ], - "time": "2016-04-11T14:30:01+00:00" + "time": "2018-02-11T19:28:00+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" } ], "packages-dev": [ @@ -365,16 +741,16 @@ }, { "name": "phpunit/php-file-iterator", - "version": "1.4.2", + "version": "1.4.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", "shasum": "" }, "require": { @@ -408,7 +784,7 @@ "filesystem", "iterator" ], - "time": "2016-10-03T07:40:28+00:00" + "time": "2017-11-27T13:52:08+00:00" }, { "name": "phpunit/php-text-template", @@ -672,22 +1048,81 @@ ], "time": "2013-01-13T10:24:48+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.9.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" + }, { "name": "symfony/yaml", - "version": "v2.8.24", + "version": "v2.8.46", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "4c29dec8d489c4e37cf87ccd7166cd0b0e6a45c5" + "reference": "5baf0f821b14eee8ca415e6a0361a9fa140c002c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/4c29dec8d489c4e37cf87ccd7166cd0b0e6a45c5", - "reference": "4c29dec8d489c4e37cf87ccd7166cd0b0e6a45c5", + "url": "https://api.github.com/repos/symfony/yaml/zipball/5baf0f821b14eee8ca415e6a0361a9fa140c002c", + "reference": "5baf0f821b14eee8ca415e6a0361a9fa140c002c", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { @@ -719,7 +1154,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-06-01T20:52:29+00:00" + "time": "2018-08-29T13:11:53+00:00" } ], "aliases": [], diff --git a/includes/WpMinions/Plugin.php b/includes/WpMinions/Plugin.php index f0c25bd..4cc166e 100644 --- a/includes/WpMinions/Plugin.php +++ b/includes/WpMinions/Plugin.php @@ -187,6 +187,8 @@ function build_client() { return new \WpMinions\Gearman\Client(); } elseif ( 'rabbitmq' === strtolower( $backend ) ) { return new \WpMinions\RabbitMQ\Client(); + } elseif ( 'sqs' === strtolower( $backend ) ) { + return new \WpMinions\SimpleQueueService\Client(); } else { return new \WpMinions\Cron\Client(); } @@ -217,6 +219,8 @@ function build_worker() { return new \WpMinions\Gearman\Worker(); } elseif ( 'rabbitmq' === strtolower( $backend ) ) { return new \WpMinions\RabbitMQ\Worker(); + } elseif ( 'sqs' === strtolower( $backend ) ) { + return new \WpMinions\SimpleQueueService\Worker(); } else { return new \WpMinions\Cron\Worker(); } diff --git a/includes/WpMinions/SimpleQueueService/Client.php b/includes/WpMinions/SimpleQueueService/Client.php new file mode 100644 index 0000000..309d3cc --- /dev/null +++ b/includes/WpMinions/SimpleQueueService/Client.php @@ -0,0 +1,102 @@ +get_sqs_client(); + } + catch (Exception $e) { + error_log( "Fatal SQS Error: Failed to connect" ); + error_log( " Cause: " . $e->getMessage() ); + } + + return $client !== false; + } + + /** + * Adds a Job to the SQS Queue. + * + * @param string $hook The action hook name for the job + * @param array $args Optional arguments for the job + * @param string $priority Optional priority of the job (ignored for SQS) + * @return bool true or false depending on the Client + */ + public function add( $hook, $args = array(), $priority = 'normal' ) { + $job_data = array( + 'hook' => $hook, + 'args' => $args, + 'blog_id' => $this->get_blog_id(), + ); + + $client = $this->get_sqs_client(); + + if ( $client !== false ) { + $payload = json_encode( $job_data ); + $result = $client->createQueue( + array( + 'QueueName' => Connection::get_queue_name() + ) + ); + + $callable = array( $client, 'sendMessage' ); + + return call_user_func( $callable, array( + 'QueueUrl' => $result['QueueUrl'], + 'MessageBody' => $payload, + ) ); + + } else { + return false; + } + } + + /* Helpers */ + + /** + * Builds the SQS Client Instance if the extension is + * installed. Once created returns the previous instance without + * reinitialization. + * + * @return Aws\Sqs\SqsClient|false An instance of SqsClient + */ + function get_sqs_client() { + if ( is_null( $this->sqs_client ) ) { + $this->sqs_client = Connection::connect(); + } + + return $this->sqs_client; + } + + /** + * Caches and returns the current blog id for adding to the Job meta + * data. False if not a multisite install. + * + * @return int|false The current blog ids id. + */ + static function get_blog_id() { + return function_exists( 'is_multisite' ) && is_multisite() ? get_current_blog_id() : false; + } + +} diff --git a/includes/WpMinions/SimpleQueueService/Connection.php b/includes/WpMinions/SimpleQueueService/Connection.php new file mode 100644 index 0000000..6a186cc --- /dev/null +++ b/includes/WpMinions/SimpleQueueService/Connection.php @@ -0,0 +1,104 @@ + '2012-11-05', + 'region' => self::get_region_name(), + ); + + if( !empty( $awssqs_server ) ) { + if( isset( $awssqs_server['access_key'] ) && isset( $awssqs_server['secret'] ) ) { + $clientConfig['credentials'] = array( + 'key' => $awssqs_server['access_key'], + 'secret' => $awssqs_server['secret'], + ); + } + } + else { + $clientConfig['profile'] = self::get_profile_name(); + } + + return SqsClient::factory( $clientConfig ); + } else { + throw new RuntimeException('AWS SDK not loaded'); + } + } + + /** + * Builds a queue name for the async tasks. + * + * @param string $baseName The unprefixed queue name + * @return string Queue name, possibly prefixed + */ + public static function get_queue_name( $baseName = 'WP_Async_Task' ) { + $key = ''; + + if ( defined( 'WP_ASYNC_TASK_SALT' ) ) { + $key .= WP_ASYNC_TASK_SALT . '-'; + } + + $key .= $baseName; + + return $key; + } + + /** + * Retrieves the AWS region for this queue. + * Looks in the AWS_DEFAULT_REGION environment variable. + * Defaults to a hard-coded value. + * @param string $default default value + * @return string region name + */ + public static function get_region_name( $default = 'us-east-1' ) { + global $awssqs_server; + + if( isset( $awssqs_server['region'] ) ) { + return $awssqs_server['region']; + } + else if( isset( $_ENV['AWS_DEFAULT_REGION '] ) ) { + return $_ENV['AWS_DEFAULT_REGION ']; + } + else { + return $default; + } + } + + /** + * Retrieves the profile name for this queue. + * Looks in the AWS_PROFILE environment variable. + * Defaults to 'default'. + * @param string $default default value + * @return string profile name + */ + public static function get_profile_name( $default = 'default' ) { + global $awssqs_server; + + if( isset( $awssqs_server['profile'] ) ) { + return $awssqs_server['profile']; + } + else if( isset( $_ENV['AWS_PROFILE'] ) ) { + return $_ENV['AWS_PROFILE']; + } + else { + return $default; + } + } + +} \ No newline at end of file diff --git a/includes/WpMinions/SimpleQueueService/Worker.php b/includes/WpMinions/SimpleQueueService/Worker.php new file mode 100644 index 0000000..93ed0a6 --- /dev/null +++ b/includes/WpMinions/SimpleQueueService/Worker.php @@ -0,0 +1,220 @@ +sqs_client = $this->get_sqs_client(); + } + catch (Exception $e) { + error_log( "Fatal SQS Error: Failed to connect" ); + error_log( " Cause: " . $e->getMessage() ); + } + + return $this->sqs_client !== false; + } + + /** + * Executes a Job pulled from SQS. On a multisite instance + * it switches to the target site before executing the job. And the + * site is restored once executing is finished. + * + * The job data contains, + * + * 1. hook - The name of the target hook to execute + * 2. args - Optional arguments to pass to the target hook + * 3. blog_id - Optional blog on a multisite to switch to, before execution + * + * Actions are fired before and after execution of the target hook. + * + * Eg:- for the action 'foo' The order of execution of actions is, + * + * 1. wp_async_task_before_job + * 2. wp_async_task_before_job_foo + * 3. foo + * 4. wp_async_task_after_job + * 5. wp_async_task_after_job_foo + * + * @param array $job The job object data. + * @return bool False if the jobs could be executed, else never returns + */ + public function work() { + + $queue = false; + + if( false === $this->sqs_client ) { + if ( ! defined( 'PHPUNIT_RUNNER' ) ) { + error_log( 'SQSWorker could not execute: sqs_client failed to initialize' ); + } + + return false; + } + + $receiveMessageCallable = array( $this->sqs_client, 'receiveMessage' ); + + if( $queue = $this->get_queue( $this->sqs_client ) ) { + while( true ) { + + $payload = false; + $receiptHandle = false; + + try { + $receiveMessageResult = call_user_func( $receiveMessageCallable, array( + 'QueueUrl' => $queue['QueueUrl'], + 'MaxNumberOfMessages' => 1, + ) ); + + if( $receiveMessageResult instanceof \Aws\Result ) { + $messages = $receiveMessageResult->get('Messages'); + if( isset( $messages[0]['Body'] ) ) { + $payload = json_decode( $messages[0]['Body'] ); + } + if( isset( $messages[0]['ReceiptHandle'] ) ) { + $receiptHandle = $messages[0]['ReceiptHandle']; + } + } + } catch ( \Exception $e ) { + if ( ! defined( 'PHPUNIT_RUNNER' ) ) { + error_log( 'SQSWorker failed to get message: ' . $e->getMessage() ); + } + } + + if( !empty( $payload ) ) { + $this->process_payload( $payload ); + } + else { + // Sleep to let the server rest before checking the queue again. + sleep( self::DELAY_BETWEEN_ITERATIONS ); + } + + if( !empty( $receiptHandle ) ) { + $this->delete_message( $queue['QueueUrl'], $receiptHandle ); + } + + } + } + + return false; + } + + /* Helpers */ + + /** + * Builds the SQS Client Instance if the extension is + * installed. Once created returns the previous instance without + * reinitialization. + * + * @return Aws\Sqs\SqsClient|false An instance of SqsClient + */ + function get_sqs_client() { + if ( is_null( $this->sqs_client ) ) { + $this->sqs_client = Connection::connect(); + } + + return $this->sqs_client; + } + + /** + * Get the information to connect to an SQS queue + * @param Aws\Sqs\SqsClient $sqs_client + * @return Aws\Result queue information, URL in [QueueUrl] value + */ + function get_queue( $sqs_client ) { + $queue = false; + + try { + $queue = $sqs_client->createQueue( + array( + 'QueueName' => Connection::get_queue_name() + ) + ); + } catch ( \Exception $e ) { + if ( ! defined( 'PHPUNIT_RUNNER' ) ) { + error_log( 'SQSWorker failed to create queue: ' . $e->getMessage() ); + } + } + + return $queue; + } + + /** + * Process the payload from an SQS queue + * + * @param stdClass $payload + * @return void + */ + function process_payload( $payload ) { + $hook = isset( $payload->hook ) ? $payload->hook : ''; + $args = isset( $payload->args ) ? (array) $payload->args : array(); + + $switched = false; + + if ( function_exists( 'is_multisite' ) && is_multisite() && !empty( $payload->blog_id ) ) { + $blog_id = $payload->blog_id; + + if ( get_current_blog_id() !== $blog_id ) { + switch_to_blog( $blog_id ); + $switched = true; + } + } + + if( !empty( $hook ) ) { + do_action( $hook, $args ); + } + + do_action( 'wp_async_task_after_work', $payload, $this ); + + if ( $switched ) { + restore_current_blog(); + } + } + + /** + * Delete a message from an SQS queue + * + * @param string $queueURL Queue URL + * @param string $receiptHandle Unique message receipt handle + * @return Aws\Result|false + */ + function delete_message( $queueURL, $receiptHandle ) { + $deleteMessageCallable = array( $this->sqs_client, 'deleteMessage' ); + $deleteMessageResult = false; + + try { + $deleteMessageResult = call_user_func( $deleteMessageCallable, array( + 'QueueUrl' => $queueURL, + 'ReceiptHandle' => $receiptHandle, + ) ); + + } + catch ( \Exception $e ) { + if ( ! defined( 'PHPUNIT_RUNNER' ) ) { + error_log( 'SQSWorker failed to delete message: ' . $e->getMessage() ); + } + } + + return $deleteMessageResult; + } + +} diff --git a/readme.md b/readme.md index 1adc41a..feb95c3 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ WP Minions [![Build Status](https://travis-ci.org/10up/WP-Minions.svg?branch=mas ======== Provides a framework for using job queues with [WordPress](http://wordpress.org/) for asynchronous task running. -Provides an integration with [Gearman](http://gearman.org/) and [RabbitMQ](https://www.rabbitmq.com) out of the box. +Provides an integration with [Gearman](http://gearman.org/), [RabbitMQ](https://www.rabbitmq.com), and [AWS Simple Queue Service](https://aws.amazon.com/sqs/) out of the box.

@@ -36,7 +36,7 @@ if ( ! isset( $_SERVER['HTTP_HOST'] ) && defined( 'DOING_ASYNC' ) && DOING_ASYNC } ``` -4. Next, you'll need to choose your job queue system. Gearman and RabbitMQ are supported out of the box. +4. Next, you'll need to choose your job queue system. Gearman, RabbitMQ, and AWS Simple Queue Service are supported out of the box. ### Gearman @@ -151,7 +151,7 @@ Where 'n' is the number of processes you want. #### WordPress Configuration -Define the `WP_MINIONS_BACKEND` constant in your ```wp-config.php```. Valid values are `gearman` or `rabbitmq`. If left blank, it will default to a cron client. +Define the `WP_MINIONS_BACKEND` constant in your ```wp-config.php```. Valid values are `gearman`, `rabbitmq`, or `sqs`. If left blank, it will default to a cron client. ``` define( 'WP_MINIONS_BACKEND', 'gearman' ); ``` @@ -194,6 +194,70 @@ Note: For some setups, the above will not work as ```/etc/default/gearman-job-se Then restart the gearman-job-server: ```sudo service gearman-job-server restart```. +### AWS Simple Queue Service + +The Simple Queue Service requires a "key" and "secret", stored in a file. Once that is in place, we can install the WordPress plugin and set the configuration options for WordPress.\ + +#### API Credentials ("key" and "secret") + +Open [AWS IAM](https://console.aws.amazon.com/iam/home?region=us-east-1#/home) in your browser and add a user with "Programmatic access". This user needs full access to AWS SQS, which can be provided through the AmazonSQSFullAccess policy (arn:aws:iam::aws:policy/AmazonSQSFullAccess). Once the user is created, copy the "Access key ID" and "Secret access key", which you will need in the next step. + +There are multiple ways to provide AWS credentials to WP Minions: + +##### wp-config.php + +The most straightforward way to connect WP Minions with AWS is to define your AWS credentials in ```wp-config.php```: + +```php +global $awssqs_server; +$awssqs_server = array( + 'access_key' => Access Key ID, + 'secret' => Secret access key, + 'region' => Region, // optional +); +``` + +##### .aws Directory + +In the home directory of the user(s) who will be running WordPress and the `wp-minions-runner.php` script, create a directory named `.aws` and a file in that directory named `credentials`. The contents of the file shoud look like this, substituting your new "Access Key ID" and "Secret access key": + +``` +[default] +aws_access_key_id=Access Key ID +aws_secret_access_key=Secret access key +``` + +Multiple profiles can be defined in `.aws/credentials`, for example: + +``` +[default] +aws_access_key_id=Access Key ID +aws_secret_access_key=Secret access key + +[10up] +aws_access_key_id=Access Key ID +aws_secret_access_key=Secret access key +``` + +Then, specify which profile to use in `wp-config.php`: + +```php +global $awssqs_server; +$awssqs_server = array( + 'profile' => '10up', // example +); +``` + +#### WordPress Configuration + +Define the `WP_MINIONS_BACKEND` constant in your ```wp-config.php```. Valid values are `gearman`, `rabbitmq`, or `sqs`. If left blank, it will default to a cron client. +``` +define( 'WP_MINIONS_BACKEND', 'sqs' ); +``` + +#### Configure the wp-minions-runner script + +See the Gearman instructions for how to run wp-minions-runner.php automatically to process queued tasks. ## Verification