// Copyright 2011 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. package com.google.appengine.tools.pipeline.impl.tasks; import static com.google.appengine.tools.pipeline.impl.util.StringUtils.UTF_8; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.tools.pipeline.impl.QueueSettings; import com.google.appengine.tools.pipeline.impl.util.GUIDGenerator; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Properties; /** * A Task that represent a set of other tasks. * <p> * The purpose of this class is to get around two limitations in App Engine: * <ol> * <li>Only a small number (currently 5) of task queue tasks is permitted to be * part of of a data store transaction. * <li>Named task queue tasks may not be part of a data store transaction. * </ol> * * This task is used as part of a strategy to get around those limitations: * Instead of enqueueing a set of tasks, a single {@code FanoutTask} may be * enqueued that, when handled, will cause a collection of other tasks to be * enqueued. * <p> * Given a Collection of Tasks, the static method * {@link #encodeTasks(Collection)} may be used to encode the Collection into a * byte array which may be persisted to the data store. An instance of this * class may contain the data store key of the entity containing the byte array. * This single task may be enqueued as part of a data store transaction. Later, * when the task is handled, the data store key may be retrieved from an * instance of this class and the byte array retrieved from the data store. Then * the static method {@link #decodeTasks(byte[])} may be used to reconstitute * the original Collection of Tasks. Finally each of the tasks in the collection * may be enqueued non-transactionally. * * @see com.google.appengine.tools.pipeline.impl.backend.PipelineBackEnd#handleFanoutTask * @author rudominer@google.com (Mitch Rudominer) */ public class FanoutTask extends Task { private static final String KEY_VALUE_SEPERATOR = "::"; private static final String PROPERTY_SEPERATOR = ",,"; private static final String TASK_NAME_DELIMITTER = "--"; private static final String TASK_SEPERATOR = ";;"; private static final String RECORD_KEY_PROPERTY = "recordKey"; private final Key recordKey; /** * Construct a new FanoutTask that contains the given data store Key. This * constructor is used to construct an instance to be enqueued. */ public FanoutTask(Key recordKey, QueueSettings queueSettings) { super(Type.FAN_OUT, null, queueSettings.clone()); this.recordKey = recordKey; } /** * Construct a new FanoutTask from the given Properties. This constructor * is used to construct an instance that is being handled. */ public FanoutTask(Type type, String taskName, Properties properties) { super(type, taskName, properties); this.recordKey = KeyFactory.stringToKey(properties.getProperty(RECORD_KEY_PROPERTY)); } @Override protected void addProperties(Properties properties) { properties.setProperty(RECORD_KEY_PROPERTY, KeyFactory.keyToString(recordKey)); } public Key getRecordKey() { return recordKey; } public static byte[] encodeTasks(Collection<? extends Task> taskList) { if (taskList.isEmpty()) { return new byte[0]; } StringBuilder builder = new StringBuilder(1024); for (Task task : taskList) { encodeTask(builder, task); builder.append(TASK_SEPERATOR); } builder.setLength(builder.length() - TASK_SEPERATOR.length()); return builder.toString().getBytes(UTF_8); } private static void encodeTask(StringBuilder builder, Task task) { String taskName = GUIDGenerator.nextGUID(); builder.append(taskName); builder.append(TASK_NAME_DELIMITTER); Properties taskProps = task.toProperties(); if (!taskProps.isEmpty()) { for (String propName : taskProps.stringPropertyNames()) { String value = taskProps.getProperty(propName); builder.append(propName).append(KEY_VALUE_SEPERATOR).append(value); builder.append(PROPERTY_SEPERATOR); } builder.setLength(builder.length() - PROPERTY_SEPERATOR.length()); } } public static List<Task> decodeTasks(byte[] encodedBytes) { String encodedListOfTasks = new String(encodedBytes, UTF_8); String[] encodedTaskArray = encodedListOfTasks.split(TASK_SEPERATOR); List<Task> listOfTasks = new ArrayList<>(encodedTaskArray.length); for (String encodedTask : encodedTaskArray) { String[] nameAndProperties = encodedTask.split(TASK_NAME_DELIMITTER); String taskName = nameAndProperties[0]; String encodedProperties = nameAndProperties[1]; String[] encodedPropertyArray = encodedProperties.split(PROPERTY_SEPERATOR); Properties taskProperties = new Properties(); for (String encodedProperty : encodedPropertyArray) { String[] keyValuePair = encodedProperty.split(KEY_VALUE_SEPERATOR); String key = keyValuePair[0]; String value = keyValuePair[1]; taskProperties.setProperty(key, value); } Task task = Task.fromProperties(taskName, taskProperties); listOfTasks.add(task); } return listOfTasks; } @Override public String propertiesAsString() { return "key=" + recordKey; } }