AEM ENGINEERING NOTES / 004

Who Should Close This ResourceResolver?

AEM Runtime · Runtime note

A practical ownership model for request and service ResourceResolvers: scope, exception paths, borrowed sessions, async work and leak-focused tests.

Mohammed Boudoun · Published

In this engineering note

01 — Start with ownership, not the variable name

Close a resolver that your code creates; do not close a resolver borrowed from the request or supplied by a caller that owns its lifecycle. A local variable named resolver does not establish ownership. Follow the acquisition path before adding close() to a finally block.

Sling documents ResourceResolver as Closeable, with a lifecycle bounded by creation and closure. Treat resources and adapted objects as scoped to that lifecycle. A helper that receives a resolver should make its borrowing contract explicit rather than unexpectedly invalidating its caller's state.

  • request.getResourceResolver(): borrowed for request processing.
  • factory.getServiceResourceResolver(...): owned by the code that acquired it.
  • resolver.clone(...): a separately acquired resolver; give the clone its own owner.
  • A resolver passed as a method parameter: ask who acquired it and who completes the operation.

02 — Make the service resolver scope visible

This method fragment assumes an injected ResourceResolverFactory and a configured content-reader subservice. The service mapping and repository permissions must be provisioned separately. Use a path fixed by the application; if input selects content, validate that input before repository access.

Return an immutable value from the bounded scope. Returning Resource, ValueMap or Session to code that runs after close() makes the ownership boundary ambiguous. Let LoginException remain distinct from content absence; a failed service login is not an empty search result.

public String readTitle() throws LoginException {
    Map<String, Object> auth = Collections.singletonMap(
        ResourceResolverFactory.SUBSERVICE, "content-reader");
    try (ResourceResolver owned = factory.getServiceResourceResolver(auth)) {
        Resource page = owned.getResource("/content/example/en/jcr:content");
        return page == null ? "" : page.getValueMap().get("jcr:title", "");
    }
}

03 — Do not give two layers the same cleanup job

When a Session is adapted from a resolver, avoid treating it as a separately logged-in session. Close the resolver you own rather than independently logging out its borrowed session. Conversely, a session acquired through a repository login has a different ownership contract. Check the actual acquisition API, not just the returned type.

Closing a resolver is also not a substitute for an explicit persistence decision. If the operation writes content, make its commit and failure handling visible. Avoid a generic cleanup helper that commits whatever changes happen to be pending: it can persist work the caller did not intend to save.

04 — Transfer a work description, not a live resolver

Sling's resolver API is generally not thread safe. A background task should acquire a resolver within its own execution scope rather than retain one from an HTTP request. Pass bounded identifiers and immutable data to a job; do not pass a request, Resource or adapted session across the boundary.

Define what happens if the target content changes before execution. For an indexing job, a missing path may mean remove the stale search document. For a publishing workflow, it may require a recorded failure. This is an application decision, not something a longer-lived resolver solves.

05 — Test the ownership contract on failure paths

A successful read test is insufficient. The useful question is whether the owned resource is released when the operation stops halfway through. Pair these checks with the Sling Model testing note when repository access sits behind a model's service dependency.

If service acquisition fails, inspect the mapping and permissions as well as component activation. The OSGi component diagnostic guide separates missing service references from failures inside an active service.

  • Verify close() on an owned resolver after both a successful read and an exception during use.
  • Verify a helper never closes a borrowed request resolver.
  • Verify login failure propagates as an operational error without attempting to close an unacquired resolver.
  • Exercise a missing resource and a denied read in integration tests; mocks cannot establish production permissions.
  • Check that returned values remain usable without retaining a resolver or session reference.

References

Related engineering work