Resource Management with Bracket Pattern
Safe Acquisition and Release for VTask
- Using
Resourcefor safe resource management in concurrent computations - Creating resources from
AutoCloseable, explicit acquire/release, and pure values - Composing multiple resources with
flatMapandand - Using one
Resourcemany times, nested or at once - Adding finalisers, and cleanup that runs only when the use fails
- Integrating resources with
Scopefor concurrent resource management
"Resource acquisition is initialization... the point is to tie the lifecycle of a resource to the lifetime of a local object." — Bjarne Stroustrup, creator of C++, on the RAII pattern that inspired functional bracket semantics
The Resource type provides safe resource management for VTask computations, implementing the bracket pattern (acquire-use-release). Resources are always released, even when exceptions occur or tasks are cancelled.
┌──────────────────────────────────────────────────────────────────┐
│ Resource Lifecycle │
│ │
│ ┌─────────┐ ┌───────────┐ ┌─────────┐ │
│ │ Acquire │ → │ Use │ → │ Release │ (guaranteed) │
│ │ resource│ │ resource │ │ resource│ │
│ └─────────┘ └───────────┘ └─────────┘ │
│ │ ↑ │
│ └── on success ──┘ │
│ └── on failure ──┘ │
│ └── on cancel ──┘ │
└──────────────────────────────────────────────────────────────────┘
Creating Resources
import org.higherkindedj.hkt.vtask.Resource;
import org.higherkindedj.hkt.vtask.VTask;
// Create a Resource from AutoCloseable (most common pattern)
Resource<Connection> connResource = Resource.fromAutoCloseable(
() -> dataSource.getConnection()
);
// Use the resource - automatically closed after use
VTask<List<User>> users = connResource.use(conn ->
VTask.of(() -> userDao.findAll(conn))
);
// Run the task - resource is managed automatically
List<User> result = users.run();
// Create a Resource with explicit acquire/release
Resource<FileChannel> fileResource = Resource.make(
() -> FileChannel.open(path, StandardOpenOption.READ),
channel -> {
try { channel.close(); }
catch (Exception e) { /* log and ignore */ }
}
);
// Use a pure value (no resource management needed)
Resource<Config> configResource = Resource.pure(loadedConfig);
Factory Methods
| Method | Description | Use Case |
|---|---|---|
fromAutoCloseable(supplier) | Wraps an AutoCloseable, closing it after each use | Database connections, streams, channels |
make(acquire, release) | Explicit acquire and release functions | Custom resources, locks, external handles |
pure(value) | Wraps a value with no cleanup | Configuration, constants, pre-initialised values |
A Resource never holds null. An acquire that returns null, pure(null), or a map function that returns null fails the use with NullPointerException. Hold a value that may be absent as a Maybe, and acquire it with make.
An exception from close() fails the use, so a writer whose final flush fails does not report success. Exception Safety says how it is reported.
Using Resources
The use method runs a computation with the acquired resource and guarantees release:
Resource<Connection> connResource = Resource.fromAutoCloseable(
() -> dataSource.getConnection()
);
// The function receives the acquired resource
// Release happens automatically when the VTask completes
VTask<Integer> count = connResource.use(conn ->
VTask.of(() -> {
try (var stmt = conn.createStatement();
var rs = stmt.executeQuery("SELECT COUNT(*) FROM users")) {
rs.next();
return rs.getInt(1);
}
})
);
// Resource is acquired when run() is called
// Resource is released when the computation completes (success or failure)
int userCount = count.run();
Exception Safety
If the use function throws, the resource is still released:
VTask<String> riskyOperation = connResource.use(conn ->
VTask.of(() -> {
if (someCondition) {
throw new RuntimeException("Something went wrong");
}
return "Success";
})
);
// Even though the computation throws, the connection is closed
Try<String> result = riskyOperation.runSafe();
// result.isFailure() == true
// connection is closed
If the release throws as well, the use's exception is still the one reported. The release's exception is added to it as a suppressed exception, as try-with-resources does. After a successful use, a release that throws fails the use with its own exception.
For fromAutoCloseable, the release is close(), which may throw a checked exception. The task that use returns reports it as it reports any checked failure: run() throws it wrapped in a VTaskExecutionException, and runSafe() returns it unwrapped in a Try.Failure. A Resource composed with flatMap or and reports a failed release as a RuntimeException instead, whose cause is the exception from the outermost release that threw.
Using One Resource Many Times
A Resource holds nothing itself. Each use acquires its own resource and releases exactly that one. So build a Resource once and use it wherever it is needed: once per request, on several threads at once, or one use nested inside another. This holds however the Resource was composed.
List<String> closed = new ArrayList<>();
AtomicInteger opened = new AtomicInteger();
Resource<String> connection =
Resource.make(() -> "conn-" + opened.incrementAndGet(), closed::add)
.map(String::toUpperCase);
List<String> both =
connection
.use(outer -> connection.use(inner -> VTask.succeed(List.of(outer, inner))))
.run();
// [CONN-1, CONN-2]
List<String> closedInOrder = List.copyOf(closed);
// [conn-2, conn-1]
The nested use opens a second connection, and each use closes the one it opened, innermost first. The map makes this a composed Resource, and its release still receives the value acquire returned.
Composing Resources
Resources compose naturally, acquiring in order and releasing in reverse (LIFO). A composed Resource can be used many times too, as Using One Resource Many Times shows.
// Chain resource acquisition with flatMap
Resource<PreparedStatement> stmtResource = connResource.flatMap(conn ->
Resource.fromAutoCloseable(() -> conn.prepareStatement(sql))
);
// Combine two independent resources with and()
Resource<Par.Tuple2<Connection, FileChannel>> combined =
connResource.and(fileResource);
combined.use(tuple -> {
Connection conn = tuple.first();
FileChannel file = tuple.second();
return VTask.of(() -> processData(conn, file));
}).run();
// fileResource released first, then connResource
// Combine three resources
Resource<Par.Tuple3<Connection, PreparedStatement, ResultSet>> triple =
connResource.and(stmtResource, resultSetResource);
// Transform resource value with map. `map` takes a plain Function, so a checked
// exception has to be dealt with here rather than propagated.
Resource<String> connectionInfo = connResource.map(conn -> {
try {
return conn.getMetaData().getURL();
} catch (SQLException e) {
throw new IllegalStateException("Cannot read connection metadata", e);
}
});
Composition Methods
| Method | Signature | Description |
|---|---|---|
map(f) | Resource<A> → (A → B) → Resource<B> | Transform the resource value |
flatMap(f) | Resource<A> → (A → Resource<B>) → Resource<B> | Chain dependent resources |
and(other) | Resource<A> → Resource<B> → Resource<Tuple2<A,B>> | Combine two resources |
and(r2, r3) | Resource<A> → Resource<B> → Resource<C> → Resource<Tuple3<A,B,C>> | Combine three resources |
Release Order
When composing resources, release order is the reverse of acquisition (LIFO):
Resource<A> ra = Resource.make(acquireA, releaseA);
Resource<B> rb = Resource.make(acquireB, releaseB);
Resource<C> rc = Resource.make(acquireC, releaseC);
// Acquisition order: A, then B, then C
// Release order: C, then B, then A
Resource<Tuple3<A, B, C>> combined = ra.and(rb, rc);
This ensures that resources depending on other resources are released first.
Resource Finalisers
Add cleanup actions that run after the primary release:
Resource<Connection> withLogging = connResource
.withFinalizer(() -> logger.info("Connection released"));
// Cleanup runs even if release throws
Resource<Lock> lockResource = Resource.make(
() -> { lock.lock(); return lock; },
Lock::unlock
).withFinalizer(() -> metrics.recordLockRelease());
Finaliser Behaviour
- Finalisers run after the primary release.
- Finalisers run in the order they were added.
- A finaliser runs even when the release throws.
- A finaliser runs even when an earlier finaliser throws.
- When the release and a finaliser both throw, the release's exception is reported. The finaliser's exception is suppressed onto it.
List<String> steps = new ArrayList<>();
Resource<String> handle =
Resource.make(() -> "handle", _ -> steps.add("release"))
.withFinalizer(() -> steps.add("log the release"))
.withFinalizer(() -> steps.add("record metrics"));
handle.useSync(String::length).run();
List<String> ranInOrder = List.copyOf(steps);
// [release, log the release, record metrics]
Resource + Scope Integration
Resources work seamlessly with Scope for structured concurrent resource management:
Resource<Connection> conn1 = Resource.fromAutoCloseable(() -> pool.getConnection());
Resource<Connection> conn2 = Resource.fromAutoCloseable(() -> pool.getConnection());
// Use resources within a scope
VTask<List<String>> parallelQueries = conn1.and(conn2).use(conns ->
Scope.<String>allSucceed()
.fork(VTask.of(() -> query(conns.first(), sql1)))
.fork(VTask.of(() -> query(conns.second(), sql2)))
.join()
);
// Both connections released after scope completes
List<String> results = parallelQueries.run();
Real-World Example: Transaction with Multiple Resources
Resource<Connection> connResource = Resource.make(
() -> {
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
return conn;
},
conn -> {
try { conn.close(); } catch (Exception e) { /* ignore */ }
}
).onFailure(conn -> {
try { conn.rollback(); } catch (Exception e) { /* ignore */ }
});
VTask<OrderResult> processOrder = connResource.use(conn ->
Scope.<Unit>allSucceed()
.fork(VTask.exec(() -> updateInventory(conn, order)))
.fork(VTask.exec(() -> chargePayment(conn, order)))
.fork(VTask.exec(() -> sendNotification(conn, order)))
.join()
.flatMap(_ -> VTask.of(() -> {
conn.commit();
return new OrderResult(order.id(), "SUCCESS");
}))
);
// If any step fails:
// 1. Scope cancels remaining tasks
// 2. onFailure rolls the transaction back
// 3. Connection is closed
Try<OrderResult> result = processOrder.runSafe();
Error Handling in Resources
onFailure Callback
onFailure adds an action that runs when the use fails, before the release. It receives the acquired resource, so it can undo partial work, such as rolling back a transaction. When the use succeeds, only the release runs.
List<String> log = new ArrayList<>();
AtomicInteger begun = new AtomicInteger();
Resource<String> transaction =
Resource.make(() -> "tx-" + begun.incrementAndGet(), tx -> log.add("close " + tx))
.onFailure(tx -> log.add("rollback " + tx));
transaction.useSync(String::length).run();
transaction.use(_ -> VTask.fail(new IllegalStateException("insert failed"))).runSafe();
List<String> whatRan = List.copyOf(log);
// [close tx-1, rollback tx-2, close tx-2]
The first use succeeds, so it only closes tx-1. The second fails, so tx-2 is rolled back and then closed. The use fails when the function given to use throws, or the task it returns fails. A cancelled task fails too. A step composed after onFailure counts as well, when it fails while the resource is held:
map's orflatMap's function throws.- The next acquire in
flatMaporandfails.
The action runs before its Resource's release and before the finalisers added to it. Actions added one after another run most recently added first. If an action throws, the release still runs, and the use's failure is still the one reported.
Combining with VTask Error Handling
VTask<Data> robust = connResource.use(conn ->
VTask.of(() -> fetchData(conn))
.recover(error -> {
logger.warn("Fetch failed, using cache", error);
return cachedData;
})
);
// Connection is released regardless of whether recover was invoked
- Resource implements the bracket pattern: acquire-use-release with guaranteed cleanup
- fromAutoCloseable wraps standard Java resources; make handles custom acquire/release
- Composition with
flatMapandandmaintains proper release ordering (LIFO) - Each use acquires its own resource, so one
Resourcecan be used many times, nested or at once - onFailure runs an action before the release when the use fails
- Finalisers add cleanup actions that run even if release throws
- Scope integration enables concurrent computations with safe resource management
- Exception safety ensures resources are released even when computations fail
Practise Resource patterns in TutorialResource (10 exercises), part of the Scope & Resource journey.
- VTask Monad - Core VTask type and basic operations
- Structured Concurrency - Scope and ScopeJoiner for task coordination
- IO Monad - Platform thread-based alternative with similar patterns
Previous: Structured Concurrency Next: VStream