1 Basic Disposable API and Concepts
A disposable is a producer of values that allocates external resources when producing a value and provides a way to deallocate those resources when a produced value is no longer needed. A value produced by a disposable is called a disposable value.
Disposables are closely related to custodians, but differ in what sort of resources they manage and how those resources are reclaimed. Broadly speaking, resources requiring management can be divided into two categories:
System resources β
resources acquired from a programβs execution environment such as memory, file descriptors, operating system threads, handles to other local processes, etc. External resources β
resources managed by arbitrary external systems that a program communicates with, such as test data in a database, network connections with graceful termination protocols, machines used to execute distributed data processing, temporary access credentials acquired from a remote key management service, etc.
These two kinds of resources have very different implications for programs that use them. Due to the distributed nature of external resources and the inherent unreliability of network communication, it is impossible to guarantee that a given external resource is released. However, system resources are very rarely impossible to release. Custodians are designed to mangage system resources and assume reliable reclamation is possible, placing several restrictions on how programs can use custodians as a result:
It must be possible to forcibly reclaim system resources during program termination as occurs when custodian-shutdown-all is called. This prevents reclaiming resources by communicating over a network because itβs impossible to guarantee successful distributed communication.
Placing a user-defined resource under the management of a custodian requires using the ffi/unsafe/custodian module. This is an unsafe API because custodian shutdown actions are executed in atomic mode, resulting in possible deadlocks if shutdown actions perform asynchronous IO.
Custodian shutdown callbacks normally should not strongly reference the values theyβre meant to clean up, as these shutdown callbacks frequently double as finalizers that run when the garbage collector determines the value to finalize is no longer reachable. This defeats certain composition patterns for shutdown callbacks, particularly composition that uses closures to reference both composed callbacks and managed values.
These restrictions make managing external resources with custodians inappropriate. Instead, an external resource should be produced by a disposable resulting in a disposable value that can be safely managed in a distributed system.
All of the bindings documented in this section are provided by disposable with the exception of acquire!, which is provided by disposable/unsafe.
1.1 Data Definition
Concretely, a disposable is implemented as a thunk that when called allocates a new disposable value and returns that value alongside another thunk that deallocates the external resources associated with that particular disposable value.
procedure
(disposable alloc dealloc) β disposable?
alloc : (-> any/c) dealloc : (-> any/c void?)
procedure
(make-disposable proc) β disposable?
proc : (-> (values any/c (-> void?)))
procedure
(disposable? v) β boolean?
v : any/c
procedure
(disposable/c c) β contract?
c : contract?
1.2 Consuming Disposable Values
Disposable values can be acquired in a variety of ways, ranging from unsafe low level access with acquire! to automated per-thread allocation with acquire-virtual.
syntax
(with-disposable ([id disp-expr] ...) body ...+)
disp-expr : disposable?
> (with-disposable ([x example-disposable] [y example-disposable]) (- x y))
Allocated 17
Allocated 40
Deallocated 17
Deallocated 40
23
> (with-disposable ([n example-disposable]) (error "uh oh!"))
Allocated 35
Deallocated 35
uh oh!
procedure
(call/disposable disp proc) β any
disp : disposable? proc : (-> any/c any)
> (call/disposable example-disposable (Ξ» (n) (* n n)))
Allocated 34
Deallocated 34
1156
> (call/disposable example-disposable (Ξ» (_) (error "uh oh!")))
Allocated 70
Deallocated 70
uh oh!
procedure
disp : disposable? evt : evt? = (thread-dead-evt (current-thread))
> (sync (thread (Ξ» () (define n (acquire example-disposable)) (printf "Acquired ~v\n" n))))
Allocated 94
Acquired 94
Deallocated 94
#<thread>
procedure
(acquire-global disp [#:plumber plumber]) β any/c
disp : disposable? plumber : plumber? = (current-plumber)
> (define plumb (make-plumber)) > (define n (acquire-global example-disposable #:plumber plumb)) Allocated 21
> (add1 n) 22
> (plumber-flush-all plumb) Deallocated 21
procedure
(acquire-virtual disp) β (-> any/c)
disp : disposable?
> (define virtual-example (acquire-virtual example-disposable))
> (define (spawn) (thread (Ξ» () (printf "Acquired ~v\n" (virtual-example))))) > (sync (spawn) (spawn) (spawn))
Allocated 28
Acquired 28
Allocated 9
Acquired 9
Allocated 13
Acquired 13
Deallocated 13
Deallocated 9
Deallocated 28
#<thread>
1.3 Unsafe Allocation of Disposables
(require disposable/unsafe) | package: disposable |
The disposable/unsafe module provides a single export, acquire!, which provides an unsafe building block upon which safe allocation abstractions can be built.
procedure
(acquire! disp) β
any/c (-> void?) disp : disposable?
> (define-values (n dispose!) (acquire! example-disposable)) Allocated 65
> (printf "Acquired ~v unsafely\n" n) Acquired 65 unsafely
> (dispose!) Deallocated 65
1.4 Monadic Composition of Disposables
procedure
(disposable-pure v) β disposable?
v : any/c
> (with-disposable ([n (disposable-pure 42)]) n) 42
procedure
(disposable-apply f disp ...) β disposable?
f : procedure? disp : disposable?
> (struct posn (x y) #:transparent)
> (define disposable-posn (disposable-apply posn example-disposable example-disposable))
> (with-disposable ([p disposable-posn]) (printf "Acquired ~v\n" p))
Allocated 65
Allocated 73
Acquired (posn 73 65)
Deallocated 65
Deallocated 73
procedure
(disposable-chain disp f) β disposable?
disp : disposable? f : (-> any/c disposable?)
> (define (add-example x) (disposable-apply (Ξ» (y) (+ x y)) example-disposable)) > (define sum-disp (disposable-chain example-disposable add-example))
> (with-disposable ([v sum-disp]) (printf "Acquired ~v\n" v))
Allocated 45
Allocated 12
Acquired 57
Deallocated 12
Deallocated 45
1.5 Asynchronous Cleanup
procedure
(disposable/async-dealloc disp) β disposable?
disp : disposable?
> (with-disposable ([n (disposable/async-dealloc example-disposable)]) (printf "Acquired ~v\n" n))
Allocated 46
Acquired 46
Deallocated 46
> (sleep 0.1)
1.6 Disposables with Associated Custodians
procedure
(disposable/custodian disp cust) β disposable?
disp : disposable? cust : custodian?
Added in version 0.4 of package disposable.
1.7 Memoization and Disposables
procedure
(disposable/memoize f [ #:make-dict make-dict]) β (disposable/c procedure?) f : (unconstrained-domain-> disposable?) make-dict : (-> (and/c dict? dict-mutable?)) = make-hash
> (define (color+ex-disp c) (disposable-apply list (disposable-pure c) example-disposable))
> (with-disposable ([color+ex (disposable/memoize color+ex-disp)]) (displayln (color+ex "red")) (displayln (color+ex "blue")) (displayln (color+ex "red")))
Allocated 80
(red 80)
Allocated 42
(blue 42)
(red 80)
Deallocated 42
Deallocated 80
Reuse of values by the memoized f is achieved by mapping arguments (both positional and keyword) to allocated values in a mutable dictionary. Allocation of the memoized f creates that mutable dictionary using make-dict, which by default produces a mutable hash table that holds keys and values strongly.
Added in version 0.5 of package disposable.