Milesoft Task Queue Extension (task-queue-ext)

1. Overview & Purpose

The Task Queue Extension (task-queue-ext) is a unified, highly optimized, and type-safe facade library built directly on top of the official Google Cloud Tasks Java SDK. Designed specifically for the Milesoft ecosystem, it standardizes background task scheduling, asynchronous task execution, and transient/delay-driven processing workflows across downstream applications and tasks.

Key Benefits & Core Capabilities:

  • Type-Safe Payloads & Automatic Serialization: Seamlessly serializes any Java domain POJO into JSON payload buffers using a custom, factory-managed Jackson ObjectMapper (with full support for custom Jackson Module registration).
  • Flexible Execution Scheduling: Provides intuitive APIs to schedule tasks either after a specific relative duration (supporting milliseconds, seconds, minutes, hours, days, etc. via Java TimeUnit) or at a specific absolute future time (ZonedDateTime).
  • Secure Integration with Downstream APIs: Out-of-the-box support for embedding cryptographically secure Authorization headers (such as Bearer guest tokens generated dynamically by platform identity servers) inside Task HTTP requests.
  • Reliable Task Cancellation: Allows downstream applications to unschedule and remove pending, non-executed tasks from GCP Cloud Tasks using their unique resource identifiers.
  • Enterprise Configuration Alignment: Standardizes Cloud Tasks client lifecycle management, encapsulating resource cleanup and client pooling patterns securely.

2. Installation & Dependency Configuration

The task-queue-ext library is securely hosted inside our Google Cloud Artifact Registry repository.

To integrate this library into your downstream Spring Boot or Java microservice, configure your build file as follows:

2.1 Gradle Setup (build.gradle)

Apply the Google Cloud Artifact Registry plugin and register our secure Maven repository:

plugins {
    id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.5"
    id "java"
}

repositories {
    mavenLocal()
    mavenCentral()
    maven {
        url "artifactregistry://us-west2-maven.pkg.dev/milesoft-repo/repo-maven"
    }
}

dependencies {
    // Required Milesoft foundational libraries
    implementation "io.milesoft:commons:2.0.1"
    
    // Milesoft Task Queue Extension
    implementation "io.milesoft:task-queue-ext:2.0.1"
}

3. Core API Reference

The primary entry point for managing background tasks is io.milesoft.taskqueue.services.TaskQueueService.

3.1 Service Constructor

To instantiate TaskQueueService, supply the GCP environment coordinates, target queue name, a standard system Clock, and any custom Jackson serialization modules:

import com.fasterxml.jackson.databind.Module;
import java.time.Clock;

public class TaskQueueService {
    public TaskQueueService(
        Clock clock, 
        String project, 
        String location, 
        String queue, 
        Module... modules
    ) {
        // Initializes GCP queue and serialization configurations
    }
}

3.2 Main Methods

The service exposes the following public API methods for task queue operations:

Method Signature Description
String queueTask(String url, Object payload, long delay, TimeUnit unit) Queues a task with a relative execution delay. Returns the GCP Task resource name.
String queueTask(String url, Object payload, long delay, TimeUnit unit, String auth) Queues a task with a relative delay and includes a secure Authorization header.
String queueTask(String url, Object payload, ZonedDateTime until) Schedules a task to run at a specific future absolute time.
String queueTask(String url, Object payload, ZonedDateTime until, String auth) Schedules a task to run at a specific future absolute time with a secure Authorization header.
void cancelTask(String name) Permanently deletes a pending task from GCP Cloud Tasks using its resource name.

4. Configuration in Spring Boot

To expose TaskQueueService as a Spring-managed bean, define a dedicated @Configuration class and map external properties.

4.1 Define Properties (application.yml)

Externalize your environment configurations, target GCP coordinates, and endpoint URLs:

taskQueue:
  project: "your-gcp-project"
  location: "us-west2"
  queue: "prod-background-tasks"

taskUrl:
  postToLinkedIn: "https://example.milesoft.app/v1/tasks/linkedin-publish"

4.2 Establish configuration bean (TaskQueueConfig.java)

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import io.milesoft.taskqueue.services.TaskQueueService;
import java.time.Clock;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TaskQueueConfig {

    private final Clock clock;
    private final String project;
    private final String location;
    private final String queue;

    @Autowired
    public TaskQueueConfig(
        Clock clock,
        @Value("${taskQueue.project}") String project,
        @Value("${taskQueue.location}") String location,
        @Value("${taskQueue.queue}") String queue) {

        checkArgument(StringUtils.isNotBlank(project), "project must not be blank");
        checkArgument(StringUtils.isNotBlank(location), "location must not be blank");
        checkArgument(StringUtils.isNotBlank(queue), "queue must not be blank");

        this.clock = checkNotNull(clock, "clock must not be null");
        this.project = project;
        this.location = location;
        this.queue = queue;
    }

    @Bean
    public TaskQueueService taskQueueService() {
        return new TaskQueueService(clock, project, location, queue);
    }
}

5. Integrating with Background Services (Real-World Pattern)

In production Milesoft applications, task queuing is typically wrapped inside a standard @Service layer (e.g. BackgroundTaskService). This layer coordinates authentication tokens with AuthClient and manages task definitions.

Below is an implementation showing how to queue tasks with secure bearer credentials:

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.milesoft.stack.constants.AuthConstants.BEARER_PREFIX;

import com.yourcompany.myapp.api.v1.dto.PostToLinkedIn;
import io.milesoft.accounts.client.v2.AuthClient;
import io.milesoft.accounts.client.v2.dto.GuestLogin;
import io.milesoft.accounts.client.v2.dto.LoggedIn;
import io.milesoft.primitives.actor.Actor;
import io.milesoft.taskqueue.services.TaskQueueService;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class BackgroundTaskService {

    private final Clock clock;
    private final AuthClient authClient;
    private final TaskQueueService taskQueueService;

    private final String appId;
    private final String accountId;
    private final String postToLinkedInUrl;

    private final Object tokenLock = new Object();
    private final AtomicReference<String> cachedToken = new AtomicReference<>();
    private final AtomicReference<Instant> tokenExpiry = new AtomicReference<>();

    @Autowired
    public BackgroundTaskService(
        Clock clock,
        AuthClient authClient,
        TaskQueueService taskQueueService,
        @Value("${app.id}") String appId,
        @Value("${account.id}") String accountId,
        @Value("${taskUrl.postToLinkedIn}") String postToLinkedInUrl) {

        checkArgument(StringUtils.isNotBlank(appId), "appId must not be blank");
        checkArgument(StringUtils.isNotBlank(accountId), "accountId must not be blank");
        checkArgument(StringUtils.isNotBlank(postToLinkedInUrl), "postToLinkedInUrl must not be blank");

        this.clock = checkNotNull(clock, "clock must not be null");
        this.authClient = checkNotNull(authClient, "authClient must not be null");
        this.taskQueueService = checkNotNull(taskQueueService, "taskQueueService must not be null");

        this.appId = appId;
        this.accountId = accountId;
        this.postToLinkedInUrl = postToLinkedInUrl;
    }

    /**
     * Retrieves or refreshes a guest identity JWT.
     */
    private String getAuth() {
        synchronized (tokenLock) {
            final Instant now = clock.instant();
            final Instant expiry = tokenExpiry.get();
            if (cachedToken.get() == null || expiry == null
                || now.isAfter(expiry.minus(Duration.ofMinutes(15)))) {

                refreshToken();
            }
            return BEARER_PREFIX + cachedToken.get();
        }
    }

    private void refreshToken() {
        log.debug("Fetching a fresh guest auth token");

        final GuestLogin command = new GuestLogin();
        command.setDuration(Duration.ofDays(1));

        final Actor robot = Actor.of(appId, accountId);

        final LoggedIn response = authClient.guestLogin(command, robot);
        if (response == null || StringUtils.isBlank(response.getAccessToken())) {
            throw new IllegalStateException("Failed to retrieve guest auth token");
        }

        cachedToken.set(response.getAccessToken());
        tokenExpiry.set(clock.instant().plus(Duration.ofDays(1)));
    }

    /**
     * Cancels a pending background task securely.
     */
    public void cancelTask(String taskId) {
        checkArgument(StringUtils.isNotBlank(taskId), "taskId must not be blank");

        try {
            taskQueueService.cancelTask(taskId);
            log.info("Successfully cancelled task: {}", taskId);
        } catch (Exception e) {
            log.warn("Unable to cancel task: {}", taskId, e);
        }
    }

    /**
     * Schedules standard LinkedIn publishing tasks.
     */
    public String queuePostToLinkedIn(String content, ZonedDateTime until) {
        checkArgument(StringUtils.isNotBlank(content), "content must not be blank");
        checkNotNull(until, "until must not be null");

        final PostToLinkedIn dto = new PostToLinkedIn();
        dto.setContent(content);

        return taskQueueService.queueTask(postToLinkedInUrl, dto, until, getAuth());
    }
}

6. Local Development & Testing

When writing automated tests for services utilizing task-queue-ext, we highly recommend mock frameworks (e.g. Mockito) rather than establishing connections to Google Cloud Tasks servers.

6.1 Unit Test Configuration Example

Below is an example of unit-testing a wrapper background service using mock components:

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;

import io.milesoft.accounts.client.v2.AuthClient;
import io.milesoft.taskqueue.services.TaskQueueService;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class TestBackgroundTaskService {

    @Mock
    private AuthClient authClient;

    @Mock
    private TaskQueueService taskQueueService;

    private BackgroundTaskService backgroundTaskService;
    private Clock clock;

    @Before
    public void setUp() {
        MockitoAnnotations.openMocks(this);
        clock = Clock.fixed(Instant.parse("2026-07-23T12:00:00Z"), ZoneId.of("UTC"));
        
        backgroundTaskService = new BackgroundTaskService(
            clock, authClient, taskQueueService, "test-app", "test-account", "http://test-url/post"
        );
    }

    @Test
    public void testCancelTask() {
        backgroundTaskService.cancelTask("task-123");
        verify(taskQueueService, times(1)).cancelTask("task-123");
    }
}