Giter VIP home page Giter VIP logo

resourcecontexts.jl's Introduction

ResourceContexts.jl

Build Status

ResourceContexts is an experimental Julia package for composable resource management without do blocks.

Resources are objects which need cleanup code to run to finalize their state. For example,

  • Open file handles
  • Temporary files and directories
  • Background Tasks
  • Many other things which are currently handled with do-blocks.

The @! macro calls or defines "context functions" — functions which take an AbstractContext as the first argument and associate any resources with that context. This package provides context-based overrides for Base functions open, mktemp, mktempdir, cd, run, lock and redirect_{stdout,stderr,stdin}.

The @context macro associates a context with the current code block which will be passed to any context functions invoked with @!. When a @context block exits the cleanup code associated with the context runs.

The @defer macro defers an arbitrary cleanup expression to the end of the current @context.

Examples

Manging resources without do blocks

Open a file, read all the lines and close it again

function f()
    @context readlines(@!(open("tmp/hi.txt", "r")))
end

Create a temporary file and ensure it's cleaned up afterward

@context function f()
    path, io = @! mktemp()
    write(io, "content")
    flush(io)
    @info "mktemp output" path ispath(path) isopen(io) read(path, String)
end

Acquire a pair of locks (and release them in the opposite order)

function f()
    lk1 = ReentrantLock()
    lk2 = ReentrantLock()
    @context begin
        @! lock(lk1)
        @! lock(lk2)
        @info "Hi from locked section" islocked(lk1) islocked(lk2)
    end
    @info "Outside locked section" islocked(lk1) islocked(lk2)
end

Start ten external processes and wait for all of them to finish before continuing

@context begin
    for i=1:10
        @! run(`sleep $(rand(2))`)
    end
end

Functions which pass resources back to their callers

Functions called as @! foo(args...) are passed the current context in the first argument; foo(current_context, args...) is called. When foo is defined using @!, the context will automatically defer resource cleanup to the caller when using @defer. For example:

Returning a bare Ptr to a temporary buffer:

@! function raw_buffer(len)
    buf = Vector{UInt8}(undef, len)
    @defer GC.@preserve buf nothing
    pointer(buf)
end

@context begin
    len = 1_000_000_000
    ptr = @! raw_buffer(len)
    GC.gc() # `buf` is preserved regardless of this call to gc()
    unsafe_store!(ptr, 0xff)
end

Defer zeroing of a secret buffer to the caller

@! function create_secret()
    buf = Base.SecretBuffer()
    write(buf, rand(UInt64)) # super secret ?
    seek(buf, 0)
    @defer Base.shred!(buf)
    buf
end

@context begin
    buf = @! create_secret()
    @info "Secret first byte" read(buf, 1)
end
# buf has been `shred!`ed at this point

Interop with "do-block-based" resource management

This is available with the enter_do function, which can "steal" the state from inside the do block and make it available in a @context block, or in the REPL:

function resource_func(f::Function, arg)
    @info "Setting up resources"
    fake_resource = 40
    f(fake_resource + arg)
    @info "Tear down resources"
end

# Normal usage
resource_func(2) do x
    @info "Resource ready" x
end

# Safely access the resource in the REPL
x = @! enter_do(resource_func, 2)

Interop with finalizer-based resource management

The special function @! detach_context_cleanup(x) can be used to detach context cleanup from the current @context block and associate it with the finalization of x instead. That is, it turns lexical resource management into dynamic resource management.

For example, to create a temporary directory with two files in it, return the directory name as a string and only clean up the directory when dir is finalized:

dir = @context begin
    dir = @! mktempdir()
    write(joinpath(dir, "file1.txt"), "Some content")
    write(joinpath(dir, "file2.txt"), "Some other content")
    @! ResourceContexts.detach_context_cleanup(dir)
end

Design

The standard solution for Julian resource management is still the do block, but this has some severe ergonomic disadvantages:

  • It's extremely inconvenient at the REPL; you cannot work with the intermediate open resources without entering the context of the do block.
  • It creates highly nested code when many resources are present. This is both visually confusing and the excess nesting leads to very deep stack traces.
  • Custom cleanup code is separated from the resource creation in a finally block.

The ergonomic factors mean that people often prefer the non-scoped form as argued here. However this also suffers some severe disadantages:

  • Resources leak (or must be finalized by the GC) when people forget to guard resource cleanup with a try ... finally.
  • Finalizers run in a restricted environment where any errors occur outside the original context where the resource was created. This makes for unstructured error handling where it's impossible to propagate errors in a natural way.
  • Functions which return objects must keep the backing resources alive by holding references to them somewhere. There's two ways to do this:
    • Have the returned object hold a reference to each resource. This is bad for the implementer because it reduces composability: one cannot combine any desired return type with arbitrary backing resources.
    • Have multiple returns such as (object,resource). This is unnatural because it forces the user to unpack return values.

The solution

This package uses the macro @! as a proxy for the proposed postfix ! syntax and adds some new ideas:

The user should not be able to "forget the !". We prevent this by introducing a new context calling convention for resource creation functions where the current AbstractContext is passed as the first argument. The @context macro creates a new context in lexical scope and the @! macro is syntactic sugar for calling with the current context.

Resource creation functions should be able to compose any desired object return type with arbitrary resources. This preserves the composability of the do block form by rejecting the conflation of the returned object and its backing resources. This is a break with some familiar APIs such as the standard file handles returned by open(filename) which are both a stream interface and a resource in need of cleanup.

Possible language integration

What would all this look like as a language feature?

  • @! could be replaced with a postfix ! as proposed way back in 2015 or so.
  • defer might become a keyword so that it can have special behavior such as ignoring its return value. In a similar way to the code which runs inside finally, there's no sense in having a "value returned by" defer. In particular, I've observed that it frequently leads to the introduction of temporary variables simply to transfer the result of the expression occurring prior to the defer line.
  • @context would be implicit at function boundaries, global let blocks, and potentially other scopes within functions. Getting this part correct is still a tricky design problem. For example, looping constructs should introduce an implicit context, but how then can the user disable this in particular cases?

Using the example from above, we've got

function create_secret()!
    buf = Base.SecretBuffer()
    write(buf, rand(UInt64)) # super secret ?
    seek(buf, 0)
    defer Base.shred!(buf)
end

let
    buf = create_secret()!
    @info "Secret first byte" read(buf, 1)
end # <- `buf` shredded here

One might be concerned that this definition of create_secret() hides the calling convention and that explicitly annotating the passed context might be more transparent. In that case we could go with syntax more like the existing macro annotations such as @nospecialize which attach metadata to function arguments. For example,

function create_secret(@passcontext(ctx::AbstractContext))
    buf = Base.SecretBuffer()
    write(buf, rand(UInt64)) # super secret ?
    seek(buf, 0)
    defer Base.shred!(buf)
end

References

resourcecontexts.jl's People

Contributors

c42f avatar green-nsk avatar pfitzseb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

resourcecontexts.jl's Issues

`@!` doesn't work on functions with return type in signature

For example:

> @! function g() end
g (generic function with 1 method)

> g(
g(var"#context"::ResourceContexts.AbstractContext) in Main at REPL[10]:1

but

> @! function h()::Int end
h (generic function with 1 method)

> h(
h() in Main at REPL[11]:1

Combining ideas with ContextManagers.jl

@tkf we discussed just a little about the differences between ResourceContexts vs ContextManagers in #8

If you're interested I'd like to discuss the design pros and cons a bit and see whether we can have a package with the best of both approaches.

Here's few design goals I currently have for this kind of thing

  1. API when using resources — ideally
    1. Minimal syntax; "avoid breaking function composition" — I feel for a technical computing audience, resource management is an annoying but necessary distraction from the main code paths in the program. I want to compose functions, even if some functions in the composition chain need resource management. The ! syntax is interesting for this.
    2. Make it inconvenient to leak resources by mistake. (Not a design goal to make leaks impossible, just annoying :-D)
    3. Allow resources to be available for hacking in the REPL, while also not leaking them when references to them are lost.
  2. API when defining resources / cleanup code
    1. Neat API for defining ad-hoc resource cleanup.
    2. Expressivity: cover anything you could express with a do block? Eg try-catch-finally.
    3. Compositionality of resources: It should be possible to define things like ContextManagers.SharedResource (I guess!)
  3. Efficiency
    1. For a fully general solution it kind of needs to be zero cost in "most use cases".

Do these seem like fitting goals for Julia? Are there important aspects I've missed?


For resource usage, ResourceContexts does reasonably well - it mostly succeeds in (1.i), though I don't really like the rules for the @context macro. @context could be improved by putting it at outer scope and figuring out acceptable scoping rules for each language construct. This would be necessary if ! syntax ever become a language construct. I think ResourceContexts succeeds in (1.ii). For (1.iii) it somewhat succeeds, but resources are leaked to the global context which isn't cleaned up until session exit.

For definition of resources, ResourceContexts is very good at (2.i). It currently fails at (2.ii) because there's no notion of catch block. I think it also fails at (2.iii), though I'm not 100% sure :-)

For efficiency ResourceContexts is currently quite bad because it uses a mutable boxed set of closures (yikes!) See #4 for some thoughts. As a language feature I feel it could be optimized to be zero cost provided @defer uses are inlined into their @context and loops introduce a @context scope.


Having written all this, I feel the second half the API difficulties mostly reduce to the general problem of writing state machines in a neat way. The context-passing approach of ResourceContexts is one way to get around that which simultaneously helps with (1.ii). (I guess it would be fair to say that ResourceContexts currently "reifies cleanup continuations as a list of boxed closures" 😆 😅 ? )

Name suggestion

May I suggest using @~ instead of @defer?

I understand the defer name comes from GoLang, but it feels too generic of a name.

TagBot trigger issue

This issue is used to trigger TagBot; feel free to unsubscribe.

If you haven't already, you should update your TagBot.yml to include issue comment triggers.
Please see this post on Discourse for instructions and more details.

If you'd like for me to do this for you, comment TagBot fix on this issue.
I'll open a PR within a few hours, please be patient!

Thread safety

Since

mutable struct ResourceContext <: AbstractContext
resources::Vector{Any}

is not protected by a lock, I'm guessing that the following pattern is not data-race-free?

function f()
    @context begin
        @sync begin
            @spawn readlines(@!(open("1.txt", "r")))
            @spawn readlines(@!(open("2.txt", "r")))
        end
    end
end

I just noticed it while reading the code and it's not causing a real issue to me. But I thought it is worth bringing it up (so that, e.g., users browsing the issue notice it).

Lexical lifetimes in Julia

A quick idea: Julia has subtyping and it can express contravariant reified generic types, so as a result its type system should be powerful enough to express references that have a lexical lifetime corresponding to a @context.

As a result, if @context generates a lifetime type that subtypes its parent context if it has one, or a stand in for the global scope if it doesn't, then we can define a LivesAtleast type as follows, which would enable functions called with @! to turn use after frees into MethodErrors:

abstract type GlobalScope end
abstract type _LifeTime{S<:GlobalScope} end
const LifeTime{S<:GlobalScope} = _LifeTime{>:S}

struct LivesExactly{L<:LifeTime,T}
    val::T
    LivesExactly(lt::Type{L},val::T) where {T,L<:LifeTime} = new{L,T}(val)
end

const LivesAtLeast{L,T} = LivesExactly{<:L,T}


# Example in use:

# Examples of lifetimes generated by scope macro.
abstract type Scope1 <: GlobalScope end
abstract type Scope2 <: Scope1 end
abstract type Scope3 <: GlobalScope end

# Simplified version of what would be generated by the @! function   macro
function foo(context_lifetime::Type{L} ,x::LivesAtLeast{L,Int}) where {L<:LifeTime} 
    x.val 
end

g = LivesExactly(LifeTime{GlobalScope}, 10)
a = LivesExactly(LifeTime{Scope1}, 10)
b = LivesExactly(LifeTime{Scope2}, 10)
c = LivesExactly(LifeTime{Scope3}, 10)

# Example of context-aware function calls with @!
foo(LifeTime{Scope1},a)           # Works (exactly correct lifetime)
foo(LifeTime{Scope1},g)           # Works (g outlives the context of the call)

foo(LifeTime{Scope1},b)          # MethodError (possible use after free on b)
foo(LifeTime{Scope1},c)          # MethodError  (unrelated lifetimes)

This isn't quite as powerful as what you would get in something like Rust that has first class lifetimes (the above provides no protection against aliasing, no linear/affine types, and no non-lexical behaviour for now), but it could be used to completely prevent dangling references or use after free bugs, and to significantly improve the ergonomics of calling into Rust with further library support.

This could be worth exploring in more detail in an experimental fork

Status?

I like this idea. I'm curious what you're thinking about its future. Do you think this is the right design? Should packages start trying to use it? Should it be integrated into the language?

Interrupts of enter_do not handled when running in VS Code

Interrupting an enter_do call in VS code while it is running the pre-user_func portion of foo leads to an exception, but the code continues running. The following should work as an MWE: run test() and interrupt with Ctrl+C when it's printing the at-info statements.

using ResourceContexts

function foo(f, x)
    @info "foo: 1" x; sleep(3)
    @info "foo: 2" x; sleep(3)
    @info "foo: 3" x; sleep(3)
    f(x); sleep(3)
    @info "foo: 4" x; sleep(3)
    @info "foo: 5" x
end

function test()
    @context begin
        x = @! enter_do(foo, 10)
        @show x
    end
end
You should get this error (hidden).
ERROR: InterruptException:
Stacktrace:
  [1] try_yieldto(undo::typeof(identity))
    @ Base ./task.jl:871
  [2] yieldto
    @ ./task.jl:856 [inlined]
  [3] yieldto
    @ ./task.jl:849 [inlined]
  [4] enter_do(#context::ResourceContext, func::typeof(foo), args::Int64; kws::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ ResourceContexts ~/.julia/packages/ResourceContexts/36Ca8/src/ResourceContexts.jl:243
  [5] enter_do
    @ ~/.julia/packages/ResourceContexts/36Ca8/src/ResourceContexts.jl:219 [inlined]
  [6] macro expansion
    @ ~/.julia/packages/ResourceContexts/36Ca8/src/ResourceContexts.jl:156 [inlined]
  [7] macro expansion
    @ ~/jc/juliahubdata-test/resourcecontext/resourcecontext.jl:23 [inlined]
  [8] macro expansion
    @ ~/.julia/packages/ResourceContexts/36Ca8/src/ResourceContexts.jl:122 [inlined]
  [9] test()
    @ Main ~/jc/juliahubdata-test/resourcecontext/resourcecontext.jl:22
 [10] top-level scope
    @ REPL[6]:1

At the same time, you should see that the at-info statements still keep printing. Furthermore, after this the REPL becomes unusable with Task not runnable errors:

julia> 2+2
ERROR: schedule: Task not runnable
Stacktrace:
  [1] error(s::String)
    @ Base ./error.jl:35
  [2] schedule(t::Task, arg::Any; error::Bool)
    @ Base ./task.jl:791
...

Now, I should emphasize that I only see this happening in the VS Code extension, and not in the normal REPL. In the normal REPL, you can see that the interrupt seems to be passed on to the inner task just fine. I haven't tried e.g. Jupyter or Pluto notebooks.

Exception stacktrace in the normal REPL
julia> test()
┌ Info: foo: 1
└   x = 10
^C^C^CERROR: TaskFailedException
Stacktrace:
 [1] wait
   @ ./task.jl:345 [inlined]
 [2] enter_do(#context::ResourceContext, func::typeof(foo), args::Int64; kws::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
   @ ResourceContexts ~/.julia/packages/ResourceContexts/36Ca8/src/ResourceContexts.jl:250
 [3] enter_do
   @ ~/.julia/packages/ResourceContexts/36Ca8/src/ResourceContexts.jl:219 [inlined]
 [4] macro expansion
   @ ~/.julia/packages/ResourceContexts/36Ca8/src/ResourceContexts.jl:156 [inlined]
 [5] macro expansion
   @ ~/jc/juliahubdata-test/resourcecontext/resourcecontext.jl:23 [inlined]
 [6] macro expansion
   @ ~/.julia/packages/ResourceContexts/36Ca8/src/ResourceContexts.jl:122 [inlined]
 [7] test()
   @ Main ~/jc/juliahubdata-test/resourcecontext/resourcecontext.jl:22
 [8] top-level scope
   @ REPL[3]:1

    nested task error: InterruptException:
    Stacktrace:
     [1] poptask(W::Base.InvasiveLinkedListSynchronized{Task})
       @ Base ./task.jl:921
     [2] wait()
       @ Base ./task.jl:930
     [3] wait(c::Base.GenericCondition{Base.Threads.SpinLock})
       @ Base ./condition.jl:124
     [4] _trywait(t::Timer)
       @ Base ./asyncevent.jl:138
     [5] wait
       @ ./asyncevent.jl:155 [inlined]
     [6] sleep(sec::Int64)
       @ Base ./asyncevent.jl:240
     [7] foo(f::ResourceContexts.var"#7#10"{Task}, x::Int64)
       @ Main ~/jc/juliahubdata-test/resourcecontext/resourcecontext.jl:5
     [8] macro expansion
       @ ~/.julia/packages/ResourceContexts/36Ca8/src/ResourceContexts.jl:230 [inlined]
     [9] (::ResourceContexts.var"#6#9"{Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, typeof(foo), Tuple{Int64}, Task})()
       @ ResourceContexts ./task.jl:134

I assume that there is some bad interaction with some task magic that VS Code does, which conflicts with the task-based implementation of enter_do. And as such, I am actually not sure if this is an error in VS Code or here.

cc @pfitzseb

Can we make `@defer` inferrable?

It seems that context passing and cleanup has a similar structure to reverse mode AD: we accumulate a bunch of actions wrapped inside closures which must be run at the exit of a scope.

Currently ResourceContexts generates kind of bad code for the cleanup because everything becomes boxed. It's not a disaster if you're cleaning up heavy weight resources, but perhaps there's some way to use opaque closures to give the compiler insight into the list of cleanup actions?

A difficulty here is that @defer may be called within a loop to defer cleanup of a collection of objects, so the cleanup list is currently of dynamic length. It's unclear whether this is a good pattern, but if it is, maybe we should try to encapsulate the list of closures as a concrete list of zero-argument thunks which can be called with minimal overhead.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.