Skip to content

Commit

Permalink
feat(socket): work without luasocket, using luasystem instead (#171)
Browse files Browse the repository at this point in the history
All we need for the core scheduling tasks is:

-  a `sleep` function for a non-busy-loop wait
-  a `gettime` function to get actual time at usecond

So without using sockets, luasystem should be good enough. Removing LuaSocket from the rockspec is a breaking change, hence it is still installed by default. To be reviewed in a next major.
  • Loading branch information
Tieske authored May 28, 2024
1 parent 95f5875 commit 20f1cf7
Show file tree
Hide file tree
Showing 16 changed files with 155 additions and 71 deletions.
1 change: 1 addition & 0 deletions .github/workflows/unix_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ jobs:
run: |
luarocks install luacov-coveralls
luarocks install luasec
luarocks install luasystem
- name: generate test certificates
run: |
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ test: certs
$(LUA) $(DELIM) $(PKGPATH) tests/largetransfer.lua
$(LUA) $(DELIM) $(PKGPATH) tests/lock.lua
$(LUA) $(DELIM) $(PKGPATH) tests/loop_starter.lua
$(LUA) $(DELIM) $(PKGPATH) tests/no_luasocket.lua
$(LUA) $(DELIM) $(PKGPATH) tests/pause.lua
$(LUA) $(DELIM) $(PKGPATH) tests/queue.lua
$(LUA) $(DELIM) $(PKGPATH) tests/removeserver.lua
Expand Down
12 changes: 9 additions & 3 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,19 @@ <h2><a name="download"></a>Download</h2>

<h2><a name="dependencies"></a>Dependencies</h2>

<p>Copas depends on
LuaSocket, <a href="http://keplerproject.github.io/coxpcall/">Coxpcall</a> (only when using Lua 5.1), and (optionally) LuaSec.
</p>
<p>Copas depends on LuaSocket (or LuaSystem), <a href="http://keplerproject.github.io/coxpcall/">Coxpcall</a>
(only when using Lua 5.1), and (optionally) LuaSec.</p>

<h2><a name="history"></a>History</h2>

<dl class="history">
<dt><strong>Copas 4.8.0</strong> unreleased</dt>
<dd><ul>
<li>Change: Copas no longer requires LuaSocket, if no sockets are needed, LuaSystem will be enough as a fallback.</li>
<li>Feat: added <code>copas.gettime()</code>, which transparently maps to either LuaSockets or
LuaSystems implementation, ensuring independence of the availability of either one of those.</li>
</ul></dd>

<dt><strong>Copas 4.7.1</strong> [9/Mar/2024]</dt>
<dd><ul>
<li>Fix: <code>copas.removethread</code> would not remove a sleeping thread immediately (it would not execute, but
Expand Down
6 changes: 6 additions & 0 deletions docs/reference.html
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ <h3>Copas dispatcher main functions</h3>
currently running coroutine.</p>
</dd>

<dt><strong><code>number = copas.gettime()</code></strong></dt>
<dd>
<p>Returns the (fractional) number of seconds since the epoch. This directly
maps to either the LuaSocket or LuaSystem implementation of <code>gettime()</code>.</p>
</dd>

<dt><strong><code>string = copas.gettraceback([msg], [co], [skt])</code></strong></dt>
<dd>
<p>Creates a traceback (string). Can be used from custom errorhandlers to create
Expand Down
120 changes: 73 additions & 47 deletions src/copas.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,27 @@ if package.loaded["copas.http"] and (_VERSION=="Lua 5.1") then -- obsolete:
error("you must require copas before require'ing copas.http")
end

-- load either LuaSocket, or LuaSystem
local socket, system do
if pcall(require, "socket") then
-- found LuaSocket
socket = require "socket"
else
-- fallback to LuaSystem
if pcall(require, "system") then
system = require "system"
else
error("Neither LuaSocket nor LuaSystem found, Copas requires at least one of them")
end
end
end

local socket = require "socket"
local binaryheap = require "binaryheap"
local gettime = socket.gettime
local gettime = (socket or system).gettime
local ssl -- only loaded upon demand

local WATCH_DOG_TIMEOUT = 120
local UDP_DATAGRAM_MAX = socket._DATAGRAMSIZE or 8192
local UDP_DATAGRAM_MAX = (socket or {})._DATAGRAMSIZE or 8192
local TIMEOUT_PRECISION = 0.1 -- 100ms
local fnil = function() end

Expand All @@ -52,7 +65,7 @@ if _VERSION=="Lua 5.1" and not jit then -- obsolete: only for Lua 5.1 compat
end


do
if socket then
-- Redefines LuaSocket functions with coroutine safe versions (pure Lua)
-- (this allows the use of socket.http from within copas)
local err_mt = {
Expand Down Expand Up @@ -126,6 +139,8 @@ copas.autoclose = true
-- indicator for the loop running
copas.running = false

-- gettime method from either LuaSocket or LuaSystem: time in (fractional) seconds, since epoch.
copas.gettime = gettime

-------------------------------------------------------------------------------
-- Object names, to track names of thread/coroutines and sockets
Expand Down Expand Up @@ -1355,6 +1370,7 @@ do
local timeout_register = setmetatable({}, { __mode = "k" })
local time_out_thread
local timerwheel = require("timerwheel").new({
now = gettime,
precision = TIMEOUT_PRECISION,
ringsize = math.floor(60*60*24/TIMEOUT_PRECISION), -- ring size 1 day
err_handler = function(err)
Expand Down Expand Up @@ -1416,6 +1432,8 @@ end
-- a task to check ready to read events
local _readable_task = {} do

_readable_task._events = {}

local function tick(skt)
local handler = _servers[skt]
if handler then
Expand All @@ -1439,6 +1457,8 @@ end
-- a task to check ready to write events
local _writable_task = {} do

_writable_task._events = {}

local function tick(skt)
_writing:remove(skt)
_doTick(_writing:pop(skt), skt)
Expand Down Expand Up @@ -1502,59 +1522,65 @@ local _select_plain do
local last_cleansing = 0
local duration = function(t2, t1) return t2-t1 end

_select_plain = function(timeout)
local err
local now = gettime()

-- remove any closed sockets to prevent select from hanging on them
if _closed[1] then
for i, skt in ipairs(_closed) do
_closed[i] = { _reading:remove(skt), _writing:remove(skt) }
if not socket then
-- socket module unavailable, switch to luasystem sleep
_select_plain = system.sleep
else
-- use socket.select to handle socket-io
_select_plain = function(timeout)
local err
local now = gettime()

-- remove any closed sockets to prevent select from hanging on them
if _closed[1] then
for i, skt in ipairs(_closed) do
_closed[i] = { _reading:remove(skt), _writing:remove(skt) }
end
end
end

_readable_task._events, _writable_task._events, err = socket.select(_reading, _writing, timeout)
local r_events, w_events = _readable_task._events, _writable_task._events
_readable_task._events, _writable_task._events, err = socket.select(_reading, _writing, timeout)
local r_events, w_events = _readable_task._events, _writable_task._events

-- inject closed sockets in readable/writeable task so they can error out properly
if _closed[1] then
for i, skts in ipairs(_closed) do
_closed[i] = nil
r_events[#r_events+1] = skts[1]
w_events[#w_events+1] = skts[2]
-- inject closed sockets in readable/writeable task so they can error out properly
if _closed[1] then
for i, skts in ipairs(_closed) do
_closed[i] = nil
r_events[#r_events+1] = skts[1]
w_events[#w_events+1] = skts[2]
end
end
end

if duration(now, last_cleansing) > WATCH_DOG_TIMEOUT then
last_cleansing = now

-- Check all sockets selected for reading, and check how long they have been waiting
-- for data already, without select returning them as readable
for skt,time in pairs(_reading_log) do
if not r_events[skt] and duration(now, time) > WATCH_DOG_TIMEOUT then
-- This one timedout while waiting to become readable, so move
-- it in the readable list and try and read anyway, despite not
-- having been returned by select
_reading_log[skt] = nil
r_events[#r_events + 1] = skt
r_events[skt] = #r_events
if duration(now, last_cleansing) > WATCH_DOG_TIMEOUT then
last_cleansing = now

-- Check all sockets selected for reading, and check how long they have been waiting
-- for data already, without select returning them as readable
for skt,time in pairs(_reading_log) do
if not r_events[skt] and duration(now, time) > WATCH_DOG_TIMEOUT then
-- This one timedout while waiting to become readable, so move
-- it in the readable list and try and read anyway, despite not
-- having been returned by select
_reading_log[skt] = nil
r_events[#r_events + 1] = skt
r_events[skt] = #r_events
end
end
end

-- Do the same for writing
for skt,time in pairs(_writing_log) do
if not w_events[skt] and duration(now, time) > WATCH_DOG_TIMEOUT then
_writing_log[skt] = nil
w_events[#w_events + 1] = skt
w_events[skt] = #w_events
-- Do the same for writing
for skt,time in pairs(_writing_log) do
if not w_events[skt] and duration(now, time) > WATCH_DOG_TIMEOUT then
_writing_log[skt] = nil
w_events[#w_events + 1] = skt
w_events[skt] = #w_events
end
end
end
end

if err == "timeout" and #r_events + #w_events > 0 then
return nil
else
return err
if err == "timeout" and #r_events + #w_events > 0 then
return nil
else
return err
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion src/copas/lock.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
local copas = require("copas")
local gettime = require("socket").gettime
local gettime = copas.gettime

local DEFAULT_TIMEOUT = 10

Expand Down
2 changes: 1 addition & 1 deletion src/copas/queue.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
local copas = require "copas"
local gettime = require("socket").gettime
local gettime = copas.gettime
local Sema = copas.semaphore
local Lock = copas.lock

Expand Down
6 changes: 3 additions & 3 deletions tests/close.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ copas.loop(function()
-- wait in the read/write queues
copas.pause(2)
-- now we're closing the connecting_socket
close_time = socket.gettime()
close_time = copas.gettime()
print("closing client socket now, client receive and send operation should immediately error out now")
client_socket:close()

Expand All @@ -55,7 +55,7 @@ copas.loop(function()
copas.addthread(function()
local data, err = client_socket:receive(1)
print("receive result: ", tostring(data), tostring(err))
receive_end_time = socket.gettime()
receive_end_time = copas.gettime()
print("receive took: ", receive_end_time - close_time)
check_exit()
end)
Expand All @@ -66,7 +66,7 @@ copas.loop(function()
ok, err = client_socket:send(("hello world"):rep(100))
end
print("send result: ", tostring(ok), tostring(err))
send_end_time = socket.gettime()
send_end_time = copas.gettime()
print("send took: ", send_end_time - close_time)
check_exit()
end)
Expand Down
14 changes: 7 additions & 7 deletions tests/largetransfer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ local socket = require 'socket'
-- copas.debug.start()

local body = ("A"):rep(1024*1024*50) -- 50 mb string
local start = socket.gettime()
local start = copas.gettime()
local done = 0
local sparams, cparams

Expand All @@ -25,7 +25,7 @@ local function runtest()
local res, err, part = skt:receive('*a')
res = res or part
if res ~= body then print("Received doesn't match send") end
print("Server reading port 49500... Done!", socket.gettime()-start, err, #res)
print("Server reading port 49500... Done!", copas.gettime()-start, err, #res)
copas.removeserver(s1)
done = done + 1
end, sparams))
Expand All @@ -38,7 +38,7 @@ local function runtest()
local res, err, part = skt:receive('*a')
res = res or part
if res ~= body then print("Received doesn't match send") end
print("Server reading port 49501... Done!", socket.gettime()-start, err, #res)
print("Server reading port 49501... Done!", copas.gettime()-start, err, #res)
copas.removeserver(s2)
done = done + 1
end, sparams))
Expand All @@ -52,7 +52,7 @@ local function runtest()
repeat
last_byte_sent, err = skt:send(body, last_byte_sent or 1, -1)
until last_byte_sent == nil or last_byte_sent == #body
print("Client writing port 49500... Done!", socket.gettime()-start, err, #body)
print("Client writing port 49500... Done!", copas.gettime()-start, err, #body)
-- we're not closing the socket, so the Copas GC-when-idle can kick-in to clean up
skt = nil -- luacheck: ignore
done = done + 1
Expand All @@ -67,7 +67,7 @@ local function runtest()
repeat
last_byte_sent, err = skt:send(body, last_byte_sent or 1, -1)
until last_byte_sent == nil or last_byte_sent == #body
print("Client writing port 49501... Done!", socket.gettime()-start, err, #body)
print("Client writing port 49501... Done!", copas.gettime()-start, err, #body)
-- we're not closing the socket, so the Copas GC-when-idle can kick-in to clean up
skt = nil -- luacheck: ignore
done = done + 1
Expand All @@ -77,7 +77,7 @@ local function runtest()
local i = 1
while done ~= 4 do
copas.pause(1)
print(i, "seconds:", socket.gettime()-start)
print(i, "seconds:", copas.gettime()-start)
i = i + 1
if i > 60 then
print"timeout"
Expand Down Expand Up @@ -114,5 +114,5 @@ cparams = {
options = {"all", "no_sslv2", "no_sslv3", "no_tlsv1"},
}
done = 0
start = socket.gettime()
start = copas.gettime()
runtest()
2 changes: 1 addition & 1 deletion tests/lock.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package.path = string.format("../src/?.lua;%s", package.path)

local copas = require "copas"
local Lock = copas.lock
local gettime = require("socket").gettime
local gettime = copas.gettime

local test_complete = false
copas.loop(function()
Expand Down
44 changes: 44 additions & 0 deletions tests/no_luasocket.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-- make sure we are pointing to the local copas first
package.path = string.format("../src/?.lua;%s", package.path)

print([[
Testing to run Copas without LuaSocket, just LuaSystem
=============================================================================
]])

-- patch require to no longer load luasocket
local _require = require
_G.require = function(name)
if name == "socket" then
error("luasocket is not allowed in this test")
end
return _require(name)
end


local copas = require "copas"
local timer = copas.timer

local successes = 0

local t1 -- luacheck: ignore

copas.loop(function()

t1 = timer.new({
delay = 0.1,
recurring = true,
callback = function(timer_obj, params)
successes = successes + 1 -- 6 to come
if successes == 6 then
timer_obj:cancel()
end
end,
})
-- succes count = 6

end)


assert(successes == 6, "number of successes didn't match! got: "..successes)
print("test success!")
Loading

0 comments on commit 20f1cf7

Please sign in to comment.