3 * Copyright 2007 Google Inc.
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
18 * The PushTask class, which is part of the Task Queue API.
22 # Overview of TODOs(petermck) for building out the full Task Queue API:
23 # - Support additional options for PushTasks, including headers, target,
24 # payload, and retry options.
25 # - Add a PushQueue class which will support adding multiple tasks at once, plus
26 # various other queue level functionality such as FetchQueueStats.
27 # - Add PullTask class. At that point, perhaps refactor to use a Task
28 # baseclass to share code with PushTask.
29 # - Add a PullQueue class, including pull specific queue methods such as
30 # leaseTasks, DeleteTasks etc.
31 # - Consider adding a Queue base class with common functionality between Push
34 namespace google\appengine\api\taskqueue
;
36 require_once 'google/appengine/api/taskqueue/taskqueue_service_pb.php';
37 require_once 'google/appengine/api/taskqueue/TaskAlreadyExistsException.php';
38 require_once 'google/appengine/api/taskqueue/TaskQueueException.php';
39 require_once 'google/appengine/api/taskqueue/TransientTaskQueueException.php';
40 require_once 'google/appengine/runtime/ApiProxy.php';
41 require_once 'google/appengine/runtime/ApplicationError.php';
43 use \google\appengine\runtime\ApiProxy
;
44 use \google\appengine\runtime\ApplicationError
;
45 use \google\appengine\TaskQueueAddRequest
;
46 use \google\appengine\TaskQueueAddRequest\RequestMethod
;
47 use \google\appengine\TaskQueueAddResponse
;
48 use \google\appengine\TaskQueueBulkAddRequest
;
49 use \google\appengine\TaskQueueBulkAddResponse
;
50 use \google\appengine\TaskQueueServiceError\ErrorCode
;
54 * A PushTask encapsulates a unit of work that an application places onto a
55 * Push Queue for asnychronous execution. The queue executes that work by
56 * sending the task back to the application in the form of an HTTP request to
57 * one of the application's handlers.
58 * This class is immutable.
60 final class PushTask
{
62 * A task may be scheduled up to 30 days into the future.
64 const MAX_DELAY_SECONDS
= 2592000;
65 const MAX_NAME_LENGTH
= 500;
66 const MAX_TASK_SIZE_BYTES
= 102400;
67 const MAX_URL_LENGTH
= 2083;
68 const NAME_PATTERN
= '/^[a-zA-Z0-9_-]+$/';
70 private static $methods = [
71 'POST' => RequestMethod
::POST
,
72 'GET' => RequestMethod
::GET
,
73 'HEAD' => RequestMethod
::HEAD
,
74 'PUT' => RequestMethod
::PUT
,
75 'DELETE' => RequestMethod
::DELETE
78 private static $default_options = [
79 'delay_seconds' => 0.0,
91 * Construct a PushTask.
93 * @param string $url_path The path of the URL handler for this task relative
94 * to your application's root directory.
95 * @param array $query_data The data carried by task, typically in the form of
96 * a set of key value pairs. This data will be encoded using
97 * http_build_query() and will be either:
99 * <li>Added to the payload of the http request if the task's method is POST
101 * <li>Added to the URL if the task's method is GET, HEAD, or DELETE.</li>
103 * @param array $options Additional options for the task. Valid options are:
105 * <li>'method': string One of 'POST', 'GET', 'HEAD', 'PUT', 'DELETE'.
106 * Default value: 'POST'.</li>
107 * <li>'name': string Name of the task. Defaults to '' meaning the service
108 * will generate a unique task name.</li>
109 * <li>'delay_seconds': float The minimum time to wait before executing the
110 * task. Default: zero.</li>
113 public function __construct($url_path, $query_data=[], $options=[]) {
114 if (!is_string($url_path)) {
115 throw new \
InvalidArgumentException('url_path must be a string. ' .
116 'Actual type: ' . gettype($url_path));
118 if (empty($url_path) ||
$url_path[0] !== '/') {
119 throw new \
InvalidArgumentException(
120 'url_path must begin with \'/\'.');
122 if (strpos($url_path, "?") !== false) {
123 throw new \
InvalidArgumentException(
124 'query strings not allowed in url_path.');
126 if (!is_array($query_data)) {
127 throw new \
InvalidArgumentException('query_data must be an array. ' .
128 'Actual type: ' . gettype($query_data));
130 if (!is_array($options)) {
131 throw new \
InvalidArgumentException('options must be an array. ' .
132 'Actual type: ' . gettype($options));
135 $extra_options = array_diff(array_keys($options),
136 array_keys(self
::$default_options));
137 if (!empty($extra_options)) {
138 throw new \
InvalidArgumentException('Invalid options supplied: ' .
139 implode(',', $extra_options));
142 $this->url_path
= $url_path;
143 $this->query_data
= $query_data;
144 $this->options
= array_merge(self
::$default_options, $options);
146 if (!array_key_exists($this->options
['method'], self
::$methods)) {
147 throw new \
InvalidArgumentException('Invalid method: ' .
148 $this->options
['method']);
150 $name = $this->options
['name'];
151 if (!is_string($name)) {
152 throw new \
InvalidArgumentException('name must be a string. ' .
153 'Actual type: ' . gettype($name));
156 if (strlen($name) > self
::MAX_NAME_LENGTH
) {
158 throw new \
InvalidArgumentException('name exceeds maximum length of ' .
159 self
::MAX_NAME_LENGTH
. ". First $display_len characters of name: "
160 . substr($name, 0, $display_len));
162 if (!preg_match(self
::NAME_PATTERN
, $name)) {
163 throw new \
InvalidArgumentException('name must match pattern: ' .
164 self
::NAME_PATTERN
. '. name: ' . $name);
167 $delay = $this->options
['delay_seconds'];
168 if (!(is_double($delay) ||
is_long($delay))) {
169 throw new \
InvalidArgumentException(
170 'delay_seconds must be a numeric type.');
172 if ($delay < 0 ||
$delay > self
::MAX_DELAY_SECONDS
) {
173 throw new \
InvalidArgumentException(
174 'delay_seconds must be between 0 and ' . self
::MAX_DELAY_SECONDS
.
175 ' (30 days). delay_seconds: ' . $delay);
180 * Return the task's URL path.
182 * @return string The task's URL path.
184 public function getUrlPath() {
185 return $this->url_path
;
189 * Return the task's query data.
191 * @return array The task's query data.
193 public function getQueryData() {
194 return $this->query_data
;
198 * Return the task's name if it was explicitly named.
200 * @return string The task's name if it was explicity named, or empty string
201 * if it will be given a uniquely generated name in the queue.
203 public function getName() {
204 return $this->options
['name'];
208 * Return the task's execution delay, in seconds.
210 * @return float The task's execution delay in seconds.
212 public function getDelaySeconds() {
213 return $this->options
['delay_seconds'];
217 * Return the task's HTTP method.
219 * @return string The task's HTTP method, i.e. one of 'DELETE', 'GET', 'HEAD',
222 public function getMethod() {
223 return $this->options
['method'];
227 * Adds the task to a queue.
229 * @param string $queue The name of the queue to add to. Defaults to
232 * @return string The name of the task.
234 * @throws TaskAlreadyExistsException if a task of the same name already
235 * exists in the queue.
236 * @throws TaskQueueException if there was a problem using the service.
238 public function add($queue = 'default') {
239 if (!is_string($queue)) {
240 throw new \
InvalidArgumentException('query must be a string.');
242 # TODO: validate queue name length and regex.
243 return self
::addTasks([$this], $queue)[0];
246 private static function applicationErrorToException($error) {
247 switch($error->getApplicationError()) {
248 case ErrorCode
::UNKNOWN_QUEUE
:
249 return new TaskQueueException('Unknown queue');
250 case ErrorCode
::TRANSIENT_ERROR
:
251 return new TransientTaskQueueException();
252 case ErrorCode
::INTERNAL_ERROR
:
253 return new TaskQueueException('Internal error');
254 case ErrorCode
::TASK_TOO_LARGE
:
255 return new TaskQueueException('Task too large');
256 case ErrorCode
::INVALID_TASK_NAME
:
257 return new TaskQueueException('Invalid task name');
258 case ErrorCode
::INVALID_QUEUE_NAME
:
259 case ErrorCode
::TOMBSTONED_QUEUE
:
260 return new TaskQueueException('Invalid queue name');
261 case ErrorCode
::INVALID_URL
:
262 return new TaskQueueException('Invalid URL');
263 case ErrorCode
::PERMISSION_DENIED
:
264 return new TaskQueueException('Permission Denied');
266 // Both TASK_ALREADY_EXISTS and TOMBSTONED_TASK are translated into the
267 // same exception. This is in keeping with the Java API but different to
268 // the Python API. Knowing that the task is tombstoned isn't particularly
269 // interesting: the main point is that it has already been added.
270 case ErrorCode
::TASK_ALREADY_EXISTS
:
271 case ErrorCode
::TOMBSTONED_TASK
:
272 return new TaskAlreadyExistsException();
273 case ErrorCode
::INVALID_ETA
:
274 return new TaskQueueException('Invalid delay_seconds');
275 case ErrorCode
::INVALID_REQUEST
:
276 return new TaskQueueException('Invalid request');
277 case ErrorCode
::INVALID_QUEUE_MODE
:
278 return new TaskQueueException('Cannot add a PushTask to a pull queue.');
280 return new TaskQueueException(
281 'Error Code: ' . $error->getApplicationError());
285 # TODO: Move this function into a PushQueue class when we have one.
286 # Returns an array containing the name of each task added.
287 private static function addTasks($tasks, $queue) {
288 $req = new TaskQueueBulkAddRequest();
289 $resp = new TaskQueueBulkAddResponse();
292 $current_time = microtime(true);
293 foreach ($tasks as $task) {
294 $names[] = $task->getName();
295 $add = $req->addAddRequest();
296 $add->setQueueName($queue);
297 $add->setTaskName($task->getName());
298 $add->setEtaUsec(($current_time +
$task->getDelaySeconds()) * 1e6
);
299 $add->setMethod(self
::$methods[$task->getMethod()]);
300 if ($task->getMethod() == 'POST' ||
$task->getMethod() == 'PUT') {
301 $add->setUrl($task->getUrlPath());
302 if ($task->getQueryData()) {
303 $add->setBody(http_build_query($task->getQueryData()));
304 $header = $add->addHeader();
305 $header->setKey('content-type');
306 $header->setValue('application/x-www-form-urlencoded');
309 $url_path = $task->getUrlPath();
310 if ($task->getQueryData()) {
311 $url_path = $url_path . '?' .
312 http_build_query($task->getQueryData());
314 $add->setUrl($url_path);
316 if (strlen($add->getUrl()) > self
::MAX_URL_LENGTH
) {
317 throw new TaskQueueException('URL length greater than maximum of ' .
318 self
::MAX_URL_LENGTH
. '. URL: ' . $add->getUrl());
320 if ($add->byteSizePartial() > self
::MAX_TASK_SIZE_BYTES
) {
321 throw new TaskQueueException('Task greater than maximum size of ' .
322 self
::MAX_TASK_SIZE_BYTES
. '. size: ' . $add->byteSizePartial());
327 ApiProxy
::makeSyncCall('taskqueue', 'BulkAdd', $req, $resp);
328 } catch (ApplicationError
$e) {
329 throw self
::applicationErrorToException($e);
332 // Update $names with any generated task names.
333 $results = $resp->getTaskResultList();
334 foreach ($results as $index => $taskResult) {
335 if ($taskResult->hasChosenTaskName()) {
336 $names[$index] = $taskResult->getChosenTaskName();