Skip to content

Instantly share code, notes, and snippets.

@siffiejoe siffiejoe/transaction.lua

Last active Nov 18, 2015
Embed
What would you like to do?
Experimental Lua module for using transactions to manage resources.
--[[
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
You can’t perform that action at this time.