Error formatting macro: pagetree: java.lang.NullPointerException
Skip to end of metadata
Go to start of metadata
You are viewing an old version of this page. View the current version. Compare with Current  |   View Page History

Rationale

It can be difficult to manage resource lifetimes, especially when laziness is involved. Simply local scoping can be inadequate. Resource scopes can decouple resource lifetimes from creating scope.

Plan

  • Generalize from resource scopes to general scopes (succeed/fail/exit)
  • with-open et al make scopes
  • Some design work and code has been done

Use Cases

  • REPL usage: open stream and it closes before I lazily consume it. Doh!
  • Start an activity, and then have nested resource lifetime automatically tie to it
    • Task A calls Iib B which allocates resources
  • Prevent resource lifetimes from nesting
    • lib C knows it doesn't want its lifetime governed by Task A
  • All works across threads

Problems

  • Other than memory allocation, the proper freeing of resources (e.g. closing open files) is on the user
    • thus, easy to get wrong
  • 'He who acquires releases' is one strategy
    • can be automated, e.g. with-xxx
      • but with-xxx puts an envelope around the availability of the resource, tied to the acquirer
      • we trip over this when we create and return lazy seqs that reference resources
      • anywhere else?, could we limit the scope of the solution to lazy seqs?
        • even then, needs care
        • e.g. closing on seq exhaustion not triggered by partial consumption
  • piggybacking on GC gets around this
    • i.e. finalizers
    • but is bad, because we can't know when/if they will run
      • non-option?
  • reference counting works in some langs
    • but not on the JVM where aliasing is common and undetectable
    • is it possible to wrap resources with a counter/tracker?
      • open question
  • can tag things as closable and call close everywhere defensively
    • but everywhere is a big and dangerous place
  • Fundamentally - how do you know when something is (or contains) a 'resource'
    • and how do you know when no one cares about it anymore?
      • and what does it mean to care?
      • could just fold down to  - care means would be unhappy if closed
    • and what needs to be done about it when that is the case?
      • who knows what that is?
  • Is the problem as simple as closing?
    • are there ever two paths?
      • e.g. happy path saves and unhappy path discards
  • are there any similarities to exception handling?
    • one part of code has problem, another deals with it

Issues

  • Consider threads as well as nesting
  • How do scopes interact with io! and stm
  • Can we hydrate the fork/join tree where we need it?
  • How to prevent mess?
    • (when-scope :fails ...) et al are very declarative
    • but are setting up non-local effects
    • potentially leading to wtf when triggered
    • is this the right external API?
      • any way to ensure correctness
      • will we be beset with requests for order control
  • Is the nearest enclosing scope always the right one?
    • what to do if not
      • named scopes
      • scopes as args

Proposal

  • dynamic binding *scope* holds collection of things needing cleanup
  • with-open binds a new *scope*, cleans up *scope* at end
  • lib APIs can call a new fn (scope thing) to add a resource to the current bound scope
    • throws exception if no scope available
  • REPL cleans up scope before every loop

IMO this nails all the use cases and problems above, with a few
caveats that I think can be subsumed into a single example as follows:

Caller A creates a scope. Lib B combines several resources to do some
work for A, some of which will be lazy/incomplete when returning to
A. The challenge is that B wants to clean up some, but not all, of the
scoped things it used. Some possibilities:

  • You are on your own in this case. Don't use scopes.
  • Scopes are more queryable/manipulable to help lib B author, e.g. named scopes.
    • complicated!
  • Scope push: Some way to opt back out of as scope (push me to the enclosing scope).
    • complicated and difficult to reason about
  • Make resources aware of their own cleanup logic, so that B can say "I am done with C but still need D and E".
    • non-starter, pollutes every API in the universe with bookkeeping to propagate cleanup logic.
  • Leverage close-on-consume
    • This is an antipatten today, but a potentially useful optimization with scopes in play
  • Scope grab
    • Give Lib B a way to grab the scope associate with a single resource and clean it up:
    • Easier to compose than named scopes
      • C doesn't have to do anything special to be used in this way
      • B doesn't have to know anything about C's use of scopes
    • Easier to componse that scope push
      • simple inversion: cleanup what you are done with, instead of enumerating the things still in play

I believe that 95% of the current problems people experience are solved with the simple mechanism proposed here. My preference is to start with "you're on your own" and evolve to "scope grab" if necessary. The latter is an easy, non-breaking extension.

TBD to flesh out this approach

  • investigate interaction with fork/join
  • new protocol or IFn for bits of cleanup logic placed in a scope
    • handling Closeable and IFn in an if statement for now
    • code in core happens before protocols are available
  • verify that change to with-open is non-breaking for existing code

Scenarios

  1. Creating a resource whose lifetime is bounded to yours: use with-open, as you do today
  2. Creating a resource that will be passed back to a caller unconsumed: call (scope res)
    1. caller must have made a scope
    2. mandatory scope is not a limitation here, it is a sensible requirement
  3. Returning >1 resources from a function with different lifecycles and cleanup rules: not supported
    1. not going to worry about this without realistic example
  4. Consuming one resource, pass another through to caller: put with-open around the one you are consuming
  5. Decide on the fly whether to clean up or let parent do it
Labels: