Skip to content

Commit

Permalink
Merge pull request #790 from 3scale/gc-for-tables
Browse files Browse the repository at this point in the history
Workaround to make __gc work on tables
  • Loading branch information
davidor authored Jun 26, 2018
2 parents b39ebd6 + 10d9dfe commit ea79660
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- New `ssl_certificate` phase allows policies to provide certificate to terminate HTTPS connection [PR #622](https://github.com/3scale/apicast/pull/622).
- Configurable `auth_type` for the token introspection policy [PR #755](https://github.com/3scale/apicast/pull/755)
- `TimerTask` module to execute recurrent tasks that can be cancelled [PR #782](https://github.com/3scale/apicast/pull/782), [#784](https://github.com/3scale/apicast/pull/784)
- `GC` module that implements a workaround to be able to define `__gc` on tables [PR #790](https://github.com/3scale/apicast/pull/790)

### Changed

Expand Down
94 changes: 94 additions & 0 deletions gateway/src/apicast/gc.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
-- In LuaJIT and Lua 5.1, the __gc metamethod does not work in tables, it only
-- works in "userdata". This module introduces a workaround to make it work
-- with tables.

local rawgetmetatable = debug.getmetatable
local getmetatable = getmetatable
local setmetatable = setmetatable
local newproxy = newproxy
local ipairs = ipairs
local pairs = pairs
local pcall = pcall
local table = table
local unpack = unpack
local error = error
local tostring = tostring

local _M = {}

local function original_table(proxy)
return rawgetmetatable(proxy).__table
end

local function __gc(proxy)
local t = original_table(proxy)
local mt = getmetatable(proxy)

if mt and mt.__gc then mt.__gc(t) end
end

local function __tostring(proxy)
return tostring(original_table(proxy))
end

local function __call(proxy, ...)
local t = original_table(proxy)

-- Try to run __call() and if it's not possible, try to run it in a way that
-- it returns a meaningful error.
local ret = { pcall(t, ...) }
local ok = table.remove(ret, 1)

if ok then
return unpack(ret)
else
error(ret[1], 2)
end
end

local function __len(proxy)
return #(original_table(proxy))
end

local function __ipairs(proxy)
return ipairs(original_table(proxy))
end

local function __pairs(proxy)
return pairs(original_table(proxy))
end

--- Set a __gc metamethod in a table
-- @tparam table t A table
-- @tparam table metatable A table that will be used as a metatable. It needs
-- to define __gc.
function _M.set_metatable_gc(t, metatable)
setmetatable(t, metatable)

-- newproxy() returns a userdata instance
local proxy = newproxy(true)

-- We are going to define a metatable in the userdata instance to make it act
-- like a table. To do that, we'll just define the metamethods a table should
-- respond to.
local mt = getmetatable(proxy)

mt.__gc = __gc

mt.__index = t
mt.__newindex = t
mt.__table = t

mt.__call = __call
mt.__len = __len
mt.__ipairs = __ipairs
mt.__pairs = __pairs
mt.__tostring = __tostring

-- Hide the 'mt' metatable. We can access it using 'rawgetmetatable()'
mt.__metatable = metatable

return proxy
end

return _M
127 changes: 127 additions & 0 deletions spec/gc_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
local GC = require('apicast.gc')

describe('GC', function()
describe('.set_metatable_gc', function()
it('enables GC on a table', function()
local test_table = { 1, 2, 3 }
local test_metatable = { __gc = function() end }
spy.on(test_metatable, '__gc')

assert(GC.set_metatable_gc(test_table, test_metatable))
collectgarbage()

assert.spy(test_metatable.__gc).was_called()
end)

it('returns an object where we can access, add, and delete elements', function()
local test_table = { 1, 2, 3, some_key = 'some_val' }
local test_metatable = { __gc = function() end }

local table_with_gc = GC.set_metatable_gc(test_table, test_metatable)

assert.equals(3, #table_with_gc)
assert.equals(1, table_with_gc[1])
assert.equals('some_val', table_with_gc.some_key)

table_with_gc.new_key = 'new_val'
assert.equals('new_val', table_with_gc.new_key)

table_with_gc.new_key = nil
assert.is_nil(table_with_gc.new_key)
end)

it('returns an object that responds to ipairs', function()
local test_table = { 1, 2, 3, some_key = 'some_val' }
local test_metatable = { __gc = function() end }

local table_with_gc = GC.set_metatable_gc(test_table, test_metatable)

local res = {}
for _, val in ipairs(table_with_gc) do
table.insert(res, val)
end

assert.same({ 1, 2, 3 }, res)
end)

it('returns an object that responds to pairs', function()
local test_table = { 1, 2, 3, some_key = 'some_val' }
local test_metatable = { __gc = function() end }

local table_with_gc = GC.set_metatable_gc(test_table, test_metatable)

local res = {}
for k, v in pairs(table_with_gc) do
res[k] = v
end

assert.same({ [1] = 1, [2] = 2, [3] = 3, some_key = 'some_val' }, res)
end)

it('returns an object that respects the __call in the mt passed in the params', function()
local test_table = { 1, 2, 3 }
local test_metatable = {
__gc = function() end,
__call = function(_, ...)
local res = 0

for _, val in ipairs(table.pack(...)) do
res = res + val
end

return res
end
}

local table_with_gc = GC.set_metatable_gc(test_table, test_metatable)

assert.equals(3, table_with_gc(1, 2))
end)

it('returns an object that respects the __tostring in the mt passed in the params', function()
local test_table = { 1, 2, 3 }
local test_metatable = {
__gc = function() end,
__tostring = function() return '123' end
}

local table_with_gc = GC.set_metatable_gc(test_table, test_metatable)

assert.equals('123', tostring(table_with_gc))
end)

it('returns an object that returns an error when it cannot be called', function()
local test_table = { 1, 2, 3 }
local test_metatable = { __gc = function() end }

local table_with_gc = GC.set_metatable_gc(test_table, test_metatable)

local ok, err = pcall(table_with_gc, 1, 2)

assert.falsy(ok)

-- Test that the error is meaningful
assert.equals('attempt to call a table value', err)
end)

it('returns an object that has as a metatable the one sent in the params', function()
local test_table = { 1, 2, 3 }
local test_metatable = { __gc = function() end }

local table_with_gc = GC.set_metatable_gc(test_table, test_metatable)

assert.same(test_metatable, getmetatable(table_with_gc))
end)

it('returns an object that respects the __index in the mt passed in the params', function()
local test_table = { 1, 2, 3 }
local test_metatable = {
__gc = function() end,
__index = { some_func = function() return 'abc' end }
}
local table_with_gc = GC.set_metatable_gc(test_table, test_metatable)

assert.equals('abc', table_with_gc:some_func())
end)
end)
end)

0 comments on commit ea79660

Please sign in to comment.