Cannot push a task to TaskQueue inside a Datastore transaction

Hello!
I’m running an AppEngine Standard locally (DevAppServer)
Per the following documentation, it should be possible to push a task to TaskQueue inside a Datastore transaction:

https://cloud.google.com/appengine/docs/legacy/standard/java/taskqueue/push/creating-tasks#enqueuing_tasks_in_cloud_datastore_transactions

And indeed, if I run similar code like that in the snippet from the documentation inside an appengine request, it is executed without an error.
I’d like to have a similar functionality, but instead in App Engine request I’d like to run it as a standalone java utility. The following is a test snippet of what I want to achieve.

import com.google.appengine.api.datastore.*;
import com.google.appengine.api.taskqueue.*;
import com.google.appengine.tools.remoteapi.*;

import java.util.UUID;

public class Utility {

    public static void main(String[] args) throws Exception {
        try (var ignoredApiRemote = openRemoteApi(createLocalhostOptions())) {

            DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

            Transaction txn = datastore.beginTransaction(TransactionOptions.Builder.withXG(true));

            try {
                Entity entity = createTestEntity();

                datastore.put(txn, entity);

                QueueFactory.getDefaultQueue().add(txn, TaskOptions.Builder.withDefaults().url("/queue").param("command", "test-command"));

                txn.commit();
            } finally {
                if (txn.isActive()) {
                    txn.rollback();
                }
            }
        }
    }

    public static Entity createTestEntity() {
        Entity entity = new Entity("Account");
        entity.setProperty("id", UUID.randomUUID().toString());
        entity.setProperty("name", "John Doe");
        return entity;
    }

    public static AutoCloseable openRemoteApi(RemoteApiOptions options) throws Exception {
        var installer = new RemoteApiInstaller();
        installer.install(options);
        return installer::uninstall;
    }

    public static RemoteApiOptions createLocalhostOptions() {
        return new RemoteApiOptions().server("localhost", 8080)
                .remoteApiPath("/remote")
                .useDevelopmentServerCredential();
    }
}

To simulate the appengine request context I’m using RemoteApiInstaller.

If I call Queue.add with txn parameter it throws the following exception:

Exception in thread “main” com.google.appengine.api.taskqueue.TransactionalTaskException
at com.google.appengine.api.taskqueue.QueueApiHelper.translateError(QueueApiHelper.java:130)
at com.google.appengine.api.taskqueue.QueueApiHelper.translateError(QueueApiHelper.java:199)
at com.google.appengine.api.taskqueue.QueueApiHelper$1.convertException(QueueApiHelper.java:70)
at com.google.appengine.api.utils.FutureWrapper.get(FutureWrapper.java:123)
at com.google.appengine.api.utils.FutureWrapper.get(FutureWrapper.java:107)
at com.google.appengine.api.utils.FutureWrapper.get(FutureWrapper.java:107)
at com.google.appengine.api.taskqueue.QueueApiHelper.getInternal(QueueApiHelper.java:101)
at com.google.appengine.api.taskqueue.QueueImpl.add(QueueImpl.java:480)
at com.mobisystems.connect.utilities.Bla.main(Bla.java:23)
Caused by: java.lang.IllegalArgumentException: transaction has expired or is invalid
at com.google.appengine.api.datastore.DatastoreApiHelper.translateError(DatastoreApiHelper.java:67)
at com.google.appengine.api.taskqueue.QueueApiHelper.translateError(QueueApiHelper.java:132)
… 8 more

If I call Queue.add with null for transaction parameter it succeeds.

How can I achieve the transactional behavior of adding to taskQueue only if the transaction succeeds?

N.B. The following snippet doesn’t have to be executed against localhost devappserver. I tried with existing remote project / appengine and received the same error.

Hi @Nikola_Pandurski,

You can’t get the “only add the task if the database save works” behavior when running your code as a standalone Java program, because that feature depends on App Engine’s own request process and environment. It only works when your code runs inside an actual App Engine request, where the system has built-in steps to hold off adding the task until it knows the database changes have gone through successfully.