App Engine Python SDK version 1.8.1
[gae.git] / python / php / sdk / google / appengine / api / taskqueue / PushTask.php
blob5ef555e16bf2ce5a4763dbf0efc11e3ceffa1122
1 <?php
2 /**
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.
17 /**
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
32 # and Pull queues.
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;
53 /**
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 {
61 /**
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,
80 'method' => 'POST',
81 'name' => '',
84 private $url_path;
86 private $query_data;
88 private $options;
90 /**
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:
98 * <ul>
99 * <li>Added to the payload of the http request if the task's method is POST
100 * or PUT.</li>
101 * <li>Added to the URL if the task's method is GET, HEAD, or DELETE.</li>
102 * </ul>
103 * @param array $options Additional options for the task. Valid options are:
104 * <ul>
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>
111 * </ul>
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));
155 if (!empty($name)) {
156 if (strlen($name) > self::MAX_NAME_LENGTH) {
157 $display_len = 1000;
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',
220 * 'POST', 'PUT'.
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
230 * 'default'.
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.');
279 default:
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();
291 $names = [];
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');
308 } else {
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());
326 try {
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();
339 return $names;