Last active
November 18, 2015 00:31
-
-
Save siffiejoe/2c01b690bfdbc34b85a6 to your computer and use it in GitHub Desktop.
Experimental Lua module for using transactions to manage resources.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
local t = transaction( function( a, b, c ) | |
if c then c:destroy() end | |
if b then b:clear() end | |
if a then a:close() end | |
end ) | |
local a = t( f() ) | |
local b = t( a:foo() ) | |
local c = t( b:bar() ) | |
-- do something with a, b, c | |
-- ... | |
t:commit() -- or t:rollback(), or t:cleanup() | |
--]] | |
local assert = assert | |
local error = assert( error ) | |
local select = assert( select ) | |
local pcall = assert( pcall ) | |
local xpcall = assert( xpcall ) | |
local setmetatable = assert( setmetatable ) | |
local unpack = assert( unpack or table.unpack ) | |
local table_concat = assert( table.concat ) | |
local coroutine_running = assert( coroutine.running ) | |
-- better error reporting when debug module is available | |
local traceback | |
do | |
local tb = debug and debug.traceback | |
if type( tb ) == "function" then | |
traceback = function() | |
return (tb( "", 3 ):gsub( "^\n[^\n]*\n([^\n]+).*$", "%1" )) | |
end | |
else | |
traceback = function() end | |
end | |
end | |
-- map coroutines to stacks of transaction scopes | |
local threads = setmetatable( {}, { __mode = "k" } ) | |
-- fake main coroutine for Lua 5.1 | |
local MAIN_THREAD = {} | |
-- marker for preallocated slots in transactions | |
local PREALLOCATED = {} | |
-- meta table for transaction objects | |
local M_meta = { | |
__index = {}, | |
__call = function( self, v ) | |
if v == nil then | |
error( "transaction value may not be `nil`" ) | |
end | |
self[ #self ] = v -- store v in preallocated slot | |
self[ #self+1 ] = PREALLOCATED -- preallocate slot for next value | |
return v | |
end, | |
__gc = function( self ) | |
if self._active then | |
if self._traceback then | |
error( "orphaned transaction in abandoned coroutine\n" | |
.. self._traceback, 0 ) | |
else | |
error( "orphaned transaction in abandoned coroutine", 0 ) | |
end | |
end | |
end, | |
} | |
-- mimic IDisposable interface of C# | |
local function default_rollback( ... ) | |
for i = select( '#', ... ), 1, -1 do | |
local v = select( i, ... ) | |
v:dispose() | |
end | |
end | |
-- each call to the customized (x)pcall functions creates a new | |
-- transaction scope in the running thread | |
local function push_transaction_scope() | |
local current = coroutine_running() or MAIN_THREAD | |
local scopes = threads[ current ] or {} | |
threads[ current ] = scopes | |
scopes[ #scopes+1 ] = {} | |
end | |
-- the transaction code is processed and removed once the custom | |
-- (x)pcall is about to return | |
local function pop_transaction_scope() | |
local scopes = threads[ coroutine_running() or MAIN_THREAD ] | |
local scope = scopes[ #scopes ] | |
scopes[ #scopes ] = nil | |
return scope | |
end | |
local function get_transaction_scope() | |
local scopes = threads[ coroutine_running() or MAIN_THREAD ] | |
if not scopes or #scopes < 1 then | |
error( "transactions require a custom variant of (x)pcall", 3 ) | |
end | |
return scopes[ #scopes ] | |
end | |
-- process the transaction objects stored in the transaction scope | |
-- created by this call to (x)pcall | |
local function pcall_postprocess( ok, ... ) | |
local scope = pop_transaction_scope() | |
if ok then | |
-- if no error occurred, make sure that all transactions were | |
-- disarmed manually | |
local msgs = {} | |
for i = #scope, 1, -1 do | |
local t = scope[ i ] | |
msgs[ #msgs+1 ] = t._traceback | |
t._active = false -- reporting it once is enough ... | |
end | |
if #msgs > 0 then | |
error( "uncommitted transaction\n"..table_concat( msgs, "\n" ), 0 ) | |
elseif #scope > 0 then | |
error( "uncommitted transaction(s)", 2 ) | |
end | |
else | |
-- if an exception occurred, run all registered rollback functions | |
-- for this transaction scope | |
for i = #scope, 1, -1 do | |
local t = scope[ i ] | |
if t._active then | |
t._active = false | |
local f, n = t._func or default_rollback, #t | |
-- exception could have occurred when preallocating slot | |
if t[ n ] == PREALLOCATED then n = n - 1 end | |
f( unpack( t, 1, n ) ) | |
end | |
end | |
end | |
return ok, ... | |
end | |
local function _xpcall( ... ) | |
push_transaction_scope() | |
return pcall_postprocess( xpcall( ... ) ) | |
end | |
local function _pcall( ... ) | |
push_transaction_scope() | |
return pcall_postprocess( pcall( ... ) ) | |
end | |
local function _new( _, f ) | |
local t = setmetatable( { | |
-- callback function to call in case of a rollback | |
_func = f, | |
-- if a transaction is neither committed nor rolled back, the | |
-- following traceback is used to report this (only if debug | |
-- module is available) | |
_traceback = traceback(), | |
-- the `__gc` metamethod is used to warn about armed transactions | |
-- started in abandoned coroutines (Lua 5.2+) | |
_active = false, | |
-- always have one preallocated slot for the values, so that no | |
-- memory allocation error may occur between creating an object | |
-- and storing it in the transaction | |
PREALLOCATED | |
}, M_meta ) | |
local ts = get_transaction_scope() | |
ts[ #ts+1 ] = t | |
t._active = true | |
return t | |
end | |
local function pop_transaction( t ) | |
local ts = get_transaction_scope() | |
if t ~= ts[ #ts ] then | |
error( "transaction not most recent in this thread" ) | |
end | |
t._active = false | |
ts[ #ts ] = nil | |
end | |
-- disarm the given transaction (which must be the most recent in the | |
-- current transaction scope) without running the callback function | |
M_meta.__index.commit = pop_transaction | |
-- disarm the given transaction (which must be the most recent in the | |
-- current transaction scope) and run the callback function | |
function M_meta.__index:rollback() | |
pop_transaction( self ) | |
local f, n = self._func or default_rollback, #self | |
if self[ n ] == PREALLOCATED then n = n - 1 end | |
f( unpack( self, 1, n ) ) | |
end | |
-- an alias for rollback() if you think in terms of RAII | |
M_meta.__index.cleanup = M_meta.__index.rollback | |
-- return exported module functions | |
return setmetatable( { | |
xpcall = _xpcall, | |
pcall = _pcall, | |
}, { __call = _new } ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment