Skip to content

Basic support for Structured Concurrency for existing applications  #419

@paulbakker

Description

@paulbakker

This is related to but not the same as #108. Scoped Values are related to Structured Concurrency, but this discussion is about streamlining the use of Structured Concurrency without Scoped Values.

The Problem

Structured Concurrency is in the fifth preview in JDK 25 and is expected to go to it's final version without changes in the next release.
The simplest way to use the API is the following:

 try (var scope = StructuredTaskScope.open()) {
    var task1 = scope.fork(() -> restClient.get()... );
    var task2 = scope.fork(() -> restClient.get()...);

    scope.join();
    return task1.get() + task2.get();
}

In this example both forks use a restClient to make a HTTP call.
This code example is broken, because the forks won't have context propagated, so the RestClient won't have tracing, security, etc.

The out-of-the-box solution that doesn't work - Scoped Values

Structured Concurrency is designed to be used together with Scoped Values (final in JDK 25).
We would wrap the code above in a ScopedValue.where(...).run(() -> { ... block, where we have ScopedValues for tracing context, security context etc.

However, adopting ScopedValues will require changes in many places, both where the values are set, and where they are read.
Because you need an explicit ScopedValue instance for each scoped value, I don't think there's a way to integrate with Micrometer Context Propagation. However, this should be explored further as well.

Workaround without ScopedValues

The StructuredConcurrency API allows to specify a ThreadFactory when opening a task scope. This is a hook we can use to propagate context to the threads created. I've experimented with this in our internal Spring Boot Netflix codebase, and seems to be a good way to adopt Structured Concurrency without requiring Scoped Values.
This issue is to discuss how Micrometer Context Propagation can make this experience better/easier for most users.
In Spring Boot Netflix we have added the following API.

package com.netflix.springboot.concurrency;

import java.util.concurrent.StructuredTaskScope;

public class ContextAwareStructuredTaskScope {

	private final ContextAwareThreadFactory contextAwareThreadFactory;

	public ContextAwareStructuredTaskScope(ContextAwareThreadFactory contextAwareThreadFactory) {
		this.contextAwareThreadFactory = contextAwareThreadFactory;
	}

	public <T, R> StructuredTaskScope<T, R> open(StructuredTaskScope.Joiner<? super T, ? extends R> joiner) {
		return StructuredTaskScope.open(joiner,
				configuration -> configuration.withThreadFactory(contextAwareThreadFactory));
	}

	public <T> StructuredTaskScope<T, Void> open() {
		var joiner = StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow();
		return StructuredTaskScope.open(joiner,
				configuration -> configuration.withThreadFactory(contextAwareThreadFactory));
	}
}

package com.netflix.springboot.concurrency;

import io.micrometer.context.ContextSnapshotFactory;

import java.util.concurrent.ThreadFactory;

public class ContextAwareThreadFactory implements ThreadFactory {

	private final ContextSnapshotFactory contextSnapshotFactory;

	public ContextAwareThreadFactory(ContextSnapshotFactory contextSnapshotFactory) {
		this.contextSnapshotFactory = contextSnapshotFactory;
	}

	@Override
	public Thread newThread(Runnable r) {
		var contextSnapshot = contextSnapshotFactory.captureAll();
		var wrapped = contextSnapshot.wrap(r);
		return Thread.ofVirtual().unstarted(wrapped);
	}
}

A user can inject the ContextAwareStructuredTaskScope and use it as follows, context propagation will work.

 try (var scope = contextAwareStructuredTaskScope.open()) {
    scope.fork(() -> //context works here!)    

What needs to be done?

The code I shared above isn't difficult, but it requires more setup code than what most users should be concerned about.
Is there an API that could be added to Micrometer Context Propagation to streamline this further?
I discussed this with @jonatan-ivanov in person, and he suggested to start a discussion here.

Sub-issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions