Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workaround to make __gc work on tables #790

Merged
merged 3 commits into from
Jun 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)