diff options
| author | Benoit Germain <benoit.germain@ubisoft.com> | 2024-05-02 17:18:38 +0200 |
|---|---|---|
| committer | Benoit Germain <benoit.germain@ubisoft.com> | 2024-05-02 17:18:38 +0200 |
| commit | eb7c7611524ba2dd2324aa3c72142e6973912286 (patch) | |
| tree | 0db09627a85f3f9f50b2ce9f37877ffe73c7007f | |
| parent | 66499a912cbf7ca0205b68a17b430070ef94bc49 (diff) | |
| download | lanes-eb7c7611524ba2dd2324aa3c72142e6973912286.tar.gz lanes-eb7c7611524ba2dd2324aa3c72142e6973912286.tar.bz2 lanes-eb7c7611524ba2dd2324aa3c72142e6973912286.zip | |
de-megathodize lanes.configure()
| -rw-r--r-- | src/lanes.lua | 1256 |
1 files changed, 649 insertions, 607 deletions
diff --git a/src/lanes.lua b/src/lanes.lua index fd3d22b..8a2592b 100644 --- a/src/lanes.lua +++ b/src/lanes.lua | |||
| @@ -12,7 +12,7 @@ | |||
| 12 | =============================================================================== | 12 | =============================================================================== |
| 13 | 13 | ||
| 14 | Copyright (C) 2007-10 Asko Kauppi <akauppi@gmail.com> | 14 | Copyright (C) 2007-10 Asko Kauppi <akauppi@gmail.com> |
| 15 | Copyright (C) 2010-13 Benoit Germain <bnt.germain@gmail.com> | 15 | Copyright (C) 2010-24 Benoit Germain <bnt.germain@gmail.com> |
| 16 | 16 | ||
| 17 | Permission is hereby granted, free of charge, to any person obtaining a copy | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy |
| 18 | of this software and associated documentation files (the "Software"), to deal | 18 | of this software and associated documentation files (the "Software"), to deal |
| @@ -41,723 +41,765 @@ local core = require "lanes.core" | |||
| 41 | -- almost everything module() does is done by require() anyway | 41 | -- almost everything module() does is done by require() anyway |
| 42 | -- -> simply create a table, populate it, return it, and be done | 42 | -- -> simply create a table, populate it, return it, and be done |
| 43 | local lanesMeta = {} | 43 | local lanesMeta = {} |
| 44 | local lanes = setmetatable( {}, lanesMeta) | 44 | local lanes = setmetatable({}, lanesMeta) |
| 45 | 45 | ||
| 46 | -- this function is available in the public interface until it is called, after which it disappears | 46 | -- ################################################################################################# |
| 47 | lanes.configure = function( settings_) | ||
| 48 | 47 | ||
| 49 | -- This check is for sublanes requiring Lanes | 48 | -- Cache globals for code that might run under sandboxing |
| 50 | -- | 49 | -- Making copies of necessary system libs will pass them on as upvalues; |
| 51 | -- TBD: We could also have the C level expose 'string.gmatch' for us. But this is simpler. | 50 | -- only the first state doing "require 'lanes'" will need to have 'string' |
| 52 | -- | 51 | -- and 'table' visible. |
| 53 | if not string then | 52 | -- |
| 54 | error( "To use 'lanes', you will also need to have 'string' available.", 2) | 53 | local assert = assert(assert) |
| 54 | local io = assert(io) | ||
| 55 | local string_gmatch = assert(string.gmatch) | ||
| 56 | local string_format = assert(string.format) | ||
| 57 | local select = assert(select) | ||
| 58 | local setmetatable = assert(setmetatable) | ||
| 59 | local table_insert = assert(table.insert) | ||
| 60 | local type = assert(type) | ||
| 61 | local pairs = assert(pairs) | ||
| 62 | local tostring = assert(tostring) | ||
| 63 | local error = assert(error) | ||
| 64 | |||
| 65 | -- ################################################################################################# | ||
| 66 | |||
| 67 | local default_params = | ||
| 68 | { | ||
| 69 | nb_keepers = 1, | ||
| 70 | keepers_gc_threshold = -1, | ||
| 71 | on_state_create = nil, | ||
| 72 | shutdown_timeout = 0.25, | ||
| 73 | shutdown_mode = "hard", | ||
| 74 | with_timers = true, | ||
| 75 | track_lanes = false, | ||
| 76 | demote_full_userdata = nil, | ||
| 77 | verbose_errors = false, | ||
| 78 | -- LuaJIT provides a thread-unsafe allocator by default, so we need to protect it when used in parallel lanes | ||
| 79 | allocator = (package and package.loaded.jit and jit.version) and "protected" or nil, | ||
| 80 | -- it looks also like LuaJIT allocator may not appreciate direct use of its allocator for other purposes than the VM operation | ||
| 81 | internal_allocator = (package and package.loaded.jit and jit.version) and "libc" or "allocator" | ||
| 82 | } | ||
| 83 | |||
| 84 | -- ################################################################################################# | ||
| 85 | |||
| 86 | local boolean_param_checker = function(val_) | ||
| 87 | -- non-'boolean-false' should be 'boolean-true' or nil | ||
| 88 | return val_ and (val_ == true) or true | ||
| 89 | end | ||
| 90 | |||
| 91 | local param_checkers = | ||
| 92 | { | ||
| 93 | nb_keepers = function(val_) | ||
| 94 | -- nb_keepers should be a number > 0 | ||
| 95 | return type(val_) == "number" and val_ > 0 | ||
| 96 | end, | ||
| 97 | keepers_gc_threshold = function(val_) | ||
| 98 | -- keepers_gc_threshold should be a number | ||
| 99 | return type(val_) == "number" | ||
| 100 | end, | ||
| 101 | with_timers = boolean_param_checker, | ||
| 102 | allocator = function(val_) | ||
| 103 | -- can be nil, "protected", or a function | ||
| 104 | return val_ and (type(val_) == "function" or val_ == "protected") or true | ||
| 105 | end, | ||
| 106 | internal_allocator = function(val_) | ||
| 107 | -- can be "libc" or "allocator" | ||
| 108 | return val_ == "libc" or val_ == "allocator" | ||
| 109 | end, | ||
| 110 | on_state_create = function(val_) | ||
| 111 | -- on_state_create may be nil or a function | ||
| 112 | return val_ and type(val_) == "function" or true | ||
| 113 | end, | ||
| 114 | shutdown_timeout = function(val_) | ||
| 115 | -- shutdown_timeout should be a number >= 0 | ||
| 116 | return type(val_) == "number" and val_ >= 0 | ||
| 117 | end, | ||
| 118 | shutdown_mode = function(val_) | ||
| 119 | local valid_hooks = { soft = true, hard = true, call = true, ret = true, line = true, count = true } | ||
| 120 | -- shutdown_mode should be a known hook mask | ||
| 121 | return valid_hooks[val_] | ||
| 122 | end, | ||
| 123 | track_lanes = boolean_param_checker, | ||
| 124 | demote_full_userdata = boolean_param_checker, | ||
| 125 | verbose_errors = boolean_param_checker | ||
| 126 | } | ||
| 127 | |||
| 128 | -- ################################################################################################# | ||
| 129 | |||
| 130 | local params_checker = function(settings_) | ||
| 131 | if not settings_ then | ||
| 132 | return default_params | ||
| 55 | end | 133 | end |
| 56 | -- Configure called so remove metatable from lanes | 134 | -- make a copy of the table to leave the provided one unchanged, *and* to help ensure it won't change behind our back |
| 57 | setmetatable( lanes, nil) | 135 | local settings = {} |
| 58 | -- | 136 | if type(settings_) ~= "table" then |
| 59 | -- Cache globals for code that might run under sandboxing | 137 | error "Bad parameter #1 to lanes.configure(), should be a table" |
| 60 | -- | ||
| 61 | local assert = assert( assert) | ||
| 62 | local string_gmatch = assert( string.gmatch) | ||
| 63 | local string_format = assert( string.format) | ||
| 64 | local select = assert( select) | ||
| 65 | local type = assert( type) | ||
| 66 | local pairs = assert( pairs) | ||
| 67 | local tostring = assert( tostring) | ||
| 68 | local error = assert( error) | ||
| 69 | |||
| 70 | local default_params = | ||
| 71 | { | ||
| 72 | nb_keepers = 1, | ||
| 73 | keepers_gc_threshold = -1, | ||
| 74 | on_state_create = nil, | ||
| 75 | shutdown_timeout = 0.25, | ||
| 76 | shutdown_mode = "hard", | ||
| 77 | with_timers = true, | ||
| 78 | track_lanes = false, | ||
| 79 | demote_full_userdata = nil, | ||
| 80 | verbose_errors = false, | ||
| 81 | -- LuaJIT provides a thread-unsafe allocator by default, so we need to protect it when used in parallel lanes | ||
| 82 | allocator = (package.loaded.jit and jit.version) and "protected" or nil, | ||
| 83 | -- it looks also like LuaJIT allocator may not appreciate direct use of its allocator for other purposes than the VM operation | ||
| 84 | internal_allocator = (package.loaded.jit and jit.version) and "libc" or "allocator" | ||
| 85 | } | ||
| 86 | local boolean_param_checker = function( val_) | ||
| 87 | -- non-'boolean-false' should be 'boolean-true' or nil | ||
| 88 | return val_ and (val_ == true) or true | ||
| 89 | end | 138 | end |
| 90 | local param_checkers = | 139 | -- any setting unknown to Lanes raises an error |
| 91 | { | 140 | for setting, _ in pairs(settings_) do |
| 92 | nb_keepers = function( val_) | 141 | if not param_checkers[setting] then |
| 93 | -- nb_keepers should be a number > 0 | 142 | error("Unknown parameter '" .. setting .. "' in configure options") |
| 94 | return type( val_) == "number" and val_ > 0 | ||
| 95 | end, | ||
| 96 | keepers_gc_threshold = function( val_) | ||
| 97 | -- keepers_gc_threshold should be a number | ||
| 98 | return type( val_) == "number" | ||
| 99 | end, | ||
| 100 | with_timers = boolean_param_checker, | ||
| 101 | allocator = function( val_) | ||
| 102 | -- can be nil, "protected", or a function | ||
| 103 | return val_ and (type( val_) == "function" or val_ == "protected") or true | ||
| 104 | end, | ||
| 105 | internal_allocator = function( val_) | ||
| 106 | -- can be "libc" or "allocator" | ||
| 107 | return val_ == "libc" or val_ == "allocator" | ||
| 108 | end, | ||
| 109 | on_state_create = function( val_) | ||
| 110 | -- on_state_create may be nil or a function | ||
| 111 | return val_ and type( val_) == "function" or true | ||
| 112 | end, | ||
| 113 | shutdown_timeout = function( val_) | ||
| 114 | -- shutdown_timeout should be a number >= 0 | ||
| 115 | return type( val_) == "number" and val_ >= 0 | ||
| 116 | end, | ||
| 117 | shutdown_mode = function( val_) | ||
| 118 | local valid_hooks = { soft = true, hard = true, call = true, ret = true, line = true, count = true } | ||
| 119 | -- shutdown_mode should be a known hook mask | ||
| 120 | return valid_hooks[val_] | ||
| 121 | end, | ||
| 122 | track_lanes = boolean_param_checker, | ||
| 123 | demote_full_userdata = boolean_param_checker, | ||
| 124 | verbose_errors = boolean_param_checker | ||
| 125 | } | ||
| 126 | |||
| 127 | local params_checker = function( settings_) | ||
| 128 | if not settings_ then | ||
| 129 | return default_params | ||
| 130 | end | ||
| 131 | -- make a copy of the table to leave the provided one unchanged, *and* to help ensure it won't change behind our back | ||
| 132 | local settings = {} | ||
| 133 | if type( settings_) ~= "table" then | ||
| 134 | error "Bad parameter #1 to lanes.configure(), should be a table" | ||
| 135 | end | 143 | end |
| 136 | -- any setting unknown to Lanes raises an error | 144 | end |
| 137 | for setting, _ in pairs( settings_) do | 145 | -- any setting not present in the provided parameters takes the default value |
| 138 | if not param_checkers[setting] then | 146 | for key, checker in pairs(param_checkers) do |
| 139 | error( "Unknown parameter '" .. setting .. "' in configure options") | 147 | local my_param = settings_[key] |
| 140 | end | 148 | local param |
| 149 | if my_param ~= nil then | ||
| 150 | param = my_param | ||
| 151 | else | ||
| 152 | param = default_params[key] | ||
| 141 | end | 153 | end |
| 142 | -- any setting not present in the provided parameters takes the default value | 154 | if not checker(param) then |
| 143 | for key, checker in pairs( param_checkers) do | 155 | error("Bad " .. key .. ": " .. tostring(param), 2) |
| 144 | local my_param = settings_[key] | ||
| 145 | local param | ||
| 146 | if my_param ~= nil then | ||
| 147 | param = my_param | ||
| 148 | else | ||
| 149 | param = default_params[key] | ||
| 150 | end | ||
| 151 | if not checker( param) then | ||
| 152 | error( "Bad " .. key .. ": " .. tostring( param), 2) | ||
| 153 | end | ||
| 154 | settings[key] = param | ||
| 155 | end | 156 | end |
| 156 | return settings | 157 | settings[key] = param |
| 157 | end | 158 | end |
| 158 | local settings = core.configure and core.configure( params_checker( settings_)) or core.settings | 159 | return settings |
| 159 | local core_lane_new = assert( core.lane_new) | 160 | end |
| 160 | local max_prio = assert( core.max_prio) | ||
| 161 | 161 | ||
| 162 | lanes.ABOUT = | 162 | -- ################################################################################################# |
| 163 | { | ||
| 164 | author= "Asko Kauppi <akauppi@gmail.com>, Benoit Germain <bnt.germain@gmail.com>", | ||
| 165 | description= "Running multiple Lua states in parallel", | ||
| 166 | license= "MIT/X11", | ||
| 167 | copyright= "Copyright (c) 2007-10, Asko Kauppi; (c) 2011-23, Benoit Germain", | ||
| 168 | version = assert( core.version) | ||
| 169 | } | ||
| 170 | 163 | ||
| 164 | local function WR(str) | ||
| 165 | io.stderr:write(str.."\n" ) | ||
| 166 | end | ||
| 171 | 167 | ||
| 172 | -- Making copies of necessary system libs will pass them on as upvalues; | 168 | -- ################################################################################################# |
| 173 | -- only the first state doing "require 'lanes'" will need to have 'string' | ||
| 174 | -- and 'table' visible. | ||
| 175 | -- | ||
| 176 | local function WR(str) | ||
| 177 | io.stderr:write( str.."\n" ) | ||
| 178 | end | ||
| 179 | 169 | ||
| 180 | local function DUMP( tbl ) | 170 | local function DUMP(tbl) |
| 181 | if not tbl then return end | 171 | if not tbl then return end |
| 182 | local str="" | 172 | local str="" |
| 183 | for k,v in pairs(tbl) do | 173 | for k,v in pairs(tbl) do |
| 184 | str= str..k.."="..tostring(v).."\n" | 174 | str= str..k.."="..tostring(v).."\n" |
| 185 | end | ||
| 186 | WR(str) | ||
| 187 | end | 175 | end |
| 176 | WR(str) | ||
| 177 | end | ||
| 188 | 178 | ||
| 189 | 179 | -- ################################################################################################# | |
| 190 | ---=== Laning ===--- | 180 | |
| 191 | 181 | local valid_libs = | |
| 192 | -- lane_h[1..n]: lane results, same as via 'lane_h:join()' | 182 | { |
| 193 | -- lane_h[0]: can be read to make sure a thread has finished (always gives 'true') | 183 | ["package"] = true, |
| 194 | -- lane_h[-1]: error message, without propagating the error | 184 | ["table"] = true, |
| 195 | -- | 185 | ["io"] = true, |
| 196 | -- Reading a Lane result (or [0]) propagates a possible error in the lane | 186 | ["os"] = true, |
| 197 | -- (and execution does not return). Cancelled lanes give 'nil' values. | 187 | ["string"] = true, |
| 198 | -- | 188 | ["math"] = true, |
| 199 | -- lane_h.state: "pending"/"running"/"waiting"/"done"/"error"/"cancelled" | 189 | ["debug"] = true, |
| 200 | -- | 190 | ["bit32"] = true, -- Lua 5.2 only, ignored silently under 5.1 |
| 201 | -- Note: Would be great to be able to have '__ipairs' metamethod, that gets | 191 | ["utf8"] = true, -- Lua 5.3 only, ignored silently under 5.1 and 5.2 |
| 202 | -- called by 'ipairs()' function to custom iterate objects. We'd use it | 192 | ["bit"] = true, -- LuaJIT only, ignored silently under PUC-Lua |
| 203 | -- for making sure a lane has ended (results are available); not requiring | 193 | ["jit"] = true, -- LuaJIT only, ignored silently under PUC-Lua |
| 204 | -- the user to precede a loop by explicit 'h[0]' or 'h:join()'. | 194 | ["ffi"] = true, -- LuaJIT only, ignored silently under PUC-Lua |
| 205 | -- | ||
| 206 | -- Or, even better, 'ipairs()' should start valuing '__index' instead | ||
| 207 | -- of using raw reads that bypass it. | ||
| 208 | -- | ||
| 209 | ----- | ||
| 210 | -- lanes.gen( [libs_str|opt_tbl [, ...],] lane_func ) ( [...] ) -> h | ||
| 211 | -- | ||
| 212 | -- 'libs': nil: no libraries available (default) | ||
| 213 | -- "": only base library ('assert', 'print', 'unpack' etc.) | ||
| 214 | -- "math,os": math + os + base libraries (named ones + base) | ||
| 215 | -- "*": all standard libraries available | ||
| 216 | -- | ||
| 217 | -- 'opt': .priority: int (-3..+3) smaller is lower priority (0 = default) | ||
| 218 | -- | ||
| 219 | -- .globals: table of globals to set for a new thread (passed by value) | ||
| 220 | -- | 195 | -- |
| 221 | -- .required: table of packages to require | 196 | ["base"] = true, |
| 222 | -- | 197 | ["coroutine"] = true, -- part of "base" in Lua 5.1 |
| 223 | -- .gc_cb: function called when the lane handle is collected | 198 | ["lanes.core"] = true |
| 224 | -- | 199 | } |
| 225 | -- ... (more options may be introduced later) ... | ||
| 226 | -- | ||
| 227 | -- Calling with a function parameter ('lane_func') ends the string/table | ||
| 228 | -- modifiers, and prepares a lane generator. | ||
| 229 | 200 | ||
| 230 | local valid_libs = | 201 | -- ################################################################################################# |
| 231 | { | 202 | |
| 232 | ["package"] = true, | 203 | local raise_option_error = function(name_, tv_, v_) |
| 233 | ["table"] = true, | 204 | error("Bad '" .. name_ .. "' option: " .. tv_ .. " " .. string_format("%q", tostring(v_)), 4) |
| 234 | ["io"] = true, | 205 | end |
| 235 | ["os"] = true, | ||
| 236 | ["string"] = true, | ||
| 237 | ["math"] = true, | ||
| 238 | ["debug"] = true, | ||
| 239 | ["bit32"] = true, -- Lua 5.2 only, ignored silently under 5.1 | ||
| 240 | ["utf8"] = true, -- Lua 5.3 only, ignored silently under 5.1 and 5.2 | ||
| 241 | ["bit"] = true, -- LuaJIT only, ignored silently under PUC-Lua | ||
| 242 | ["jit"] = true, -- LuaJIT only, ignored silently under PUC-Lua | ||
| 243 | ["ffi"] = true, -- LuaJIT only, ignored silently under PUC-Lua | ||
| 244 | -- | ||
| 245 | ["base"] = true, | ||
| 246 | ["coroutine"] = true, -- part of "base" in Lua 5.1 | ||
| 247 | ["lanes.core"] = true | ||
| 248 | } | ||
| 249 | 206 | ||
| 250 | local raise_option_error = function( name_, tv_, v_) | 207 | -- ################################################################################################# |
| 251 | error( "Bad '" .. name_ .. "' option: " .. tv_ .. " " .. string_format( "%q", tostring( v_)), 4) | 208 | |
| 209 | local opt_validators = | ||
| 210 | { | ||
| 211 | priority = function(v_) | ||
| 212 | local tv = type(v_) | ||
| 213 | return (tv == "number") and v_ or raise_option_error("priority", tv, v_) | ||
| 214 | end, | ||
| 215 | globals = function(v_) | ||
| 216 | local tv = type(v_) | ||
| 217 | return (tv == "table") and v_ or raise_option_error("globals", tv, v_) | ||
| 218 | end, | ||
| 219 | package = function(v_) | ||
| 220 | local tv = type(v_) | ||
| 221 | return (tv == "table") and v_ or raise_option_error("package", tv, v_) | ||
| 222 | end, | ||
| 223 | required = function(v_) | ||
| 224 | local tv = type(v_) | ||
| 225 | return (tv == "table") and v_ or raise_option_error("required", tv, v_) | ||
| 226 | end, | ||
| 227 | gc_cb = function(v_) | ||
| 228 | local tv = type(v_) | ||
| 229 | return (tv == "function") and v_ or raise_option_error("gc_cb", tv, v_) | ||
| 252 | end | 230 | end |
| 231 | } | ||
| 253 | 232 | ||
| 254 | local opt_validators = | 233 | -- ############################################################################################# |
| 255 | { | 234 | -- ##################################### lanes.gen() ########################################### |
| 256 | priority = function( v_) | 235 | -- ############################################################################################# |
| 257 | local tv = type( v_) | ||
| 258 | return (tv == "number") and v_ or raise_option_error( "priority", tv, v_) | ||
| 259 | end, | ||
| 260 | globals = function( v_) | ||
| 261 | local tv = type( v_) | ||
| 262 | return (tv == "table") and v_ or raise_option_error( "globals", tv, v_) | ||
| 263 | end, | ||
| 264 | package = function( v_) | ||
| 265 | local tv = type( v_) | ||
| 266 | return (tv == "table") and v_ or raise_option_error( "package", tv, v_) | ||
| 267 | end, | ||
| 268 | required = function( v_) | ||
| 269 | local tv = type( v_) | ||
| 270 | return (tv == "table") and v_ or raise_option_error( "required", tv, v_) | ||
| 271 | end, | ||
| 272 | gc_cb = function( v_) | ||
| 273 | local tv = type( v_) | ||
| 274 | return (tv == "function") and v_ or raise_option_error( "gc_cb", tv, v_) | ||
| 275 | end | ||
| 276 | } | ||
| 277 | 236 | ||
| 278 | -- PUBLIC LANES API | 237 | -- lane_h[1..n]: lane results, same as via 'lane_h:join()' |
| 279 | -- receives a sequence of strings and tables, plus a function | 238 | -- lane_h[0]: can be read to make sure a thread has finished (always gives 'true') |
| 280 | local gen = function( ...) | 239 | -- lane_h[-1]: error message, without propagating the error |
| 281 | -- aggregrate all strings together, separated by "," as well as tables | 240 | -- |
| 282 | -- the strings are a list of libraries to open | 241 | -- Reading a Lane result (or [0]) propagates a possible error in the lane |
| 283 | -- the tables contain the lane options | 242 | -- (and execution does not return). Cancelled lanes give 'nil' values. |
| 284 | local opt = {} | 243 | -- |
| 285 | local libs = nil | 244 | -- lane_h.state: "pending"/"running"/"waiting"/"done"/"error"/"cancelled" |
| 286 | 245 | -- | |
| 287 | local n = select( '#', ...) | 246 | -- Note: Would be great to be able to have '__ipairs' metamethod, that gets |
| 288 | 247 | -- called by 'ipairs()' function to custom iterate objects. We'd use it | |
| 289 | -- we need at least a function | 248 | -- for making sure a lane has ended (results are available); not requiring |
| 290 | if n == 0 then | 249 | -- the user to precede a loop by explicit 'h[0]' or 'h:join()'. |
| 291 | error( "No parameters!", 2) | 250 | -- |
| 292 | end | 251 | -- Or, even better, 'ipairs()' should start valuing '__index' instead |
| 252 | -- of using raw reads that bypass it. | ||
| 253 | -- | ||
| 254 | ----- | ||
| 255 | -- lanes.gen([libs_str|opt_tbl [, ...],] lane_func ) ([...]) -> h | ||
| 256 | -- | ||
| 257 | -- 'libs': nil: no libraries available (default) | ||
| 258 | -- "": only base library ('assert', 'print', 'unpack' etc.) | ||
| 259 | -- "math,os": math + os + base libraries (named ones + base) | ||
| 260 | -- "*": all standard libraries available | ||
| 261 | -- | ||
| 262 | -- 'opt': .priority: int (-3..+3) smaller is lower priority (0 = default) | ||
| 263 | -- | ||
| 264 | -- .globals: table of globals to set for a new thread (passed by value) | ||
| 265 | -- | ||
| 266 | -- .required: table of packages to require | ||
| 267 | -- | ||
| 268 | -- .gc_cb: function called when the lane handle is collected | ||
| 269 | -- | ||
| 270 | -- ... (more options may be introduced later) ... | ||
| 271 | -- | ||
| 272 | -- Calling with a function parameter ('lane_func') ends the string/table | ||
| 273 | -- modifiers, and prepares a lane generator. | ||
| 274 | |||
| 275 | -- receives a sequence of strings and tables, plus a function | ||
| 276 | local gen = function(...) | ||
| 277 | -- aggregrate all strings together, separated by "," as well as tables | ||
| 278 | -- the strings are a list of libraries to open | ||
| 279 | -- the tables contain the lane options | ||
| 280 | local opt = {} | ||
| 281 | local libs = nil | ||
| 282 | |||
| 283 | local n = select('#', ...) | ||
| 284 | |||
| 285 | -- we need at least a function | ||
| 286 | if n == 0 then | ||
| 287 | error("No parameters!", 2) | ||
| 288 | end | ||
| 293 | 289 | ||
| 294 | -- all arguments but the last must be nil, strings, or tables | 290 | -- all arguments but the last must be nil, strings, or tables |
| 295 | for i = 1, n - 1 do | 291 | for i = 1, n - 1 do |
| 296 | local v = select( i, ...) | 292 | local v = select(i, ...) |
| 297 | local tv = type( v) | 293 | local tv = type(v) |
| 298 | if tv == "string" then | 294 | if tv == "string" then |
| 299 | libs = libs and libs .. "," .. v or v | 295 | libs = libs and libs .. "," .. v or v |
| 300 | elseif tv == "table" then | 296 | elseif tv == "table" then |
| 301 | for k, vv in pairs( v) do | 297 | for k, vv in pairs(v) do |
| 302 | opt[k]= vv | 298 | opt[k]= vv |
| 303 | end | ||
| 304 | elseif v == nil then | ||
| 305 | -- skip | ||
| 306 | else | ||
| 307 | error( "Bad parameter " .. i .. ": " .. tv .. " " .. string_format( "%q", tostring( v)), 2) | ||
| 308 | end | 299 | end |
| 300 | elseif v == nil then | ||
| 301 | -- skip | ||
| 302 | else | ||
| 303 | error("Bad parameter " .. i .. ": " .. tv .. " " .. string_format("%q", tostring(v)), 2) | ||
| 309 | end | 304 | end |
| 305 | end | ||
| 310 | 306 | ||
| 311 | -- the last argument should be a function or a string | 307 | -- the last argument should be a function or a string |
| 312 | local func = select( n, ...) | 308 | local func = select(n, ...) |
| 313 | local functype = type( func) | 309 | local functype = type(func) |
| 314 | if functype ~= "function" and functype ~= "string" then | 310 | if functype ~= "function" and functype ~= "string" then |
| 315 | error( "Last parameter not function or string: " .. functype .. " " .. string_format( "%q", tostring( func)), 2) | 311 | error("Last parameter not function or string: " .. functype .. " " .. string_format("%q", tostring(func)), 2) |
| 316 | end | 312 | end |
| 317 | 313 | ||
| 318 | -- check that the caller only provides reserved library names, and those only once | 314 | -- check that the caller only provides reserved library names, and those only once |
| 319 | -- "*" is a special case that doesn't require individual checking | 315 | -- "*" is a special case that doesn't require individual checking |
| 320 | if libs and libs ~= "*" then | 316 | if libs and libs ~= "*" then |
| 321 | local found = {} | 317 | local found = {} |
| 322 | for s in string_gmatch(libs, "[%a%d.]+") do | 318 | for s in string_gmatch(libs, "[%a%d.]+") do |
| 323 | if not valid_libs[s] then | 319 | if not valid_libs[s] then |
| 324 | error( "Bad library name: " .. s, 2) | 320 | error("Bad library name: " .. s, 2) |
| 325 | else | 321 | else |
| 326 | found[s] = (found[s] or 0) + 1 | 322 | found[s] = (found[s] or 0) + 1 |
| 327 | if found[s] > 1 then | 323 | if found[s] > 1 then |
| 328 | error( "libs specification contains '" .. s .. "' more than once", 2) | 324 | error("libs specification contains '" .. s .. "' more than once", 2) |
| 329 | end | ||
| 330 | end | 325 | end |
| 331 | end | 326 | end |
| 332 | end | 327 | end |
| 328 | end | ||
| 333 | 329 | ||
| 334 | -- validate that each option is known and properly valued | 330 | -- validate that each option is known and properly valued |
| 335 | for k, v in pairs( opt) do | 331 | for k, v in pairs(opt) do |
| 336 | local validator = opt_validators[k] | 332 | local validator = opt_validators[k] |
| 337 | if not validator then | 333 | if not validator then |
| 338 | error( (type( k) == "number" and "Unkeyed option: " .. type( v) .. " " .. string_format( "%q", tostring( v)) or "Bad '" .. tostring( k) .. "' option"), 2) | 334 | error((type(k) == "number" and "Unkeyed option: " .. type(v) .. " " .. string_format("%q", tostring(v)) or "Bad '" .. tostring(k) .. "' option"), 2) |
| 339 | else | 335 | else |
| 340 | opt[k] = validator( v) | 336 | opt[k] = validator(v) |
| 341 | end | ||
| 342 | end | 337 | end |
| 338 | end | ||
| 343 | 339 | ||
| 344 | local priority, globals, package, required, gc_cb = opt.priority, opt.globals, opt.package or package, opt.required, opt.gc_cb | 340 | local core_lane_new = assert(core.lane_new) |
| 345 | return function( ...) | 341 | local priority, globals, package, required, gc_cb = opt.priority, opt.globals, opt.package or package, opt.required, opt.gc_cb |
| 346 | -- must pass functions args last else they will be truncated to the first one | 342 | return function(...) |
| 347 | return core_lane_new( func, libs, priority, globals, package, required, gc_cb, ...) | 343 | -- must pass functions args last else they will be truncated to the first one |
| 348 | end | 344 | return core_lane_new(func, libs, priority, globals, package, required, gc_cb, ...) |
| 349 | end -- gen() | 345 | end |
| 346 | end -- gen() | ||
| 350 | 347 | ||
| 351 | ---=== Timers ===--- | 348 | -- ################################################################################################# |
| 349 | -- ####################################### Timers ################################################## | ||
| 350 | -- ################################################################################################# | ||
| 352 | 351 | ||
| 353 | -- PUBLIC LANES API | 352 | -- PUBLIC LANES API |
| 354 | local timer = function() error "timers are not active" end | 353 | local timer = function() error "timers are not active" end |
| 355 | local timers = timer | 354 | local timers = timer |
| 356 | local timer_lane = nil | 355 | local timer_lane = nil |
| 357 | 356 | ||
| 358 | -- timer_gateway should always exist, even when the settings disable the timers | 357 | -- timer_gateway should always exist, even when the settings disable the timers |
| 359 | local timer_gateway = assert( core.timer_gateway) | 358 | local timer_gateway |
| 360 | 359 | ||
| 361 | ----- | 360 | local TGW_KEY = "(timer control)" -- the key does not matter, a 'weird' key may help debugging |
| 362 | -- <void> = sleep( [seconds_]) | 361 | local TGW_QUERY, TGW_REPLY = "(timer query)", "(timer reply)" |
| 363 | -- | ||
| 364 | -- PUBLIC LANES API | ||
| 365 | local sleep = function( seconds_) | ||
| 366 | seconds_ = seconds_ or 0.0 -- this causes false and nil to be a valid input, equivalent to 0.0, but that's ok | ||
| 367 | if type( seconds_) ~= "number" then | ||
| 368 | error( "invalid duration " .. string_format( "%q", tostring(seconds_))) | ||
| 369 | end | ||
| 370 | -- receive data on a channel no-one ever sends anything, thus blocking for the specified duration | ||
| 371 | return timer_gateway:receive( seconds_, "ac100de1-a696-4619-b2f0-a26de9d58ab8") | ||
| 372 | end | ||
| 373 | 362 | ||
| 363 | -- ################################################################################################# | ||
| 374 | 364 | ||
| 375 | if settings.with_timers ~= false then | 365 | local configure_timers = function() |
| 376 | -- | 366 | -- On first 'require "lanes"', a timer lane is spawned that will maintain |
| 377 | -- On first 'require "lanes"', a timer lane is spawned that will maintain | 367 | -- timer tables and sleep in between the timer events. All interaction with |
| 378 | -- timer tables and sleep in between the timer events. All interaction with | 368 | -- the timer lane happens via a 'timer_gateway' Linda, which is common to |
| 379 | -- the timer lane happens via a 'timer_gateway' Linda, which is common to | 369 | -- all that 'require "lanes"'. |
| 380 | -- all that 'require "lanes"'. | 370 | -- |
| 381 | -- | 371 | -- Linda protocol to timer lane: |
| 382 | -- Linda protocol to timer lane: | 372 | -- |
| 383 | -- | 373 | -- TGW_KEY: linda_h, key, [wakeup_at_secs], [repeat_secs] |
| 384 | -- TGW_KEY: linda_h, key, [wakeup_at_secs], [repeat_secs] | ||
| 385 | -- | ||
| 386 | local TGW_KEY= "(timer control)" -- the key does not matter, a 'weird' key may help debugging | ||
| 387 | local TGW_QUERY, TGW_REPLY = "(timer query)", "(timer reply)" | ||
| 388 | local first_time_key= "first time" | ||
| 389 | 374 | ||
| 390 | local first_time = timer_gateway:get( first_time_key) == nil | 375 | local now_secs = core.now_secs |
| 391 | timer_gateway:set( first_time_key, true) | 376 | local wakeup_conv = core.wakeup_conv |
| 392 | 377 | ||
| 393 | local now_secs = core.now_secs | 378 | -- Timer lane; initialize only on the first 'require "lanes"' instance (which naturally has 'table' always declared) |
| 394 | local wakeup_conv = core.wakeup_conv | 379 | local first_time_key = "first time" |
| 380 | local first_time = timer_gateway:get(first_time_key) == nil | ||
| 381 | timer_gateway:set(first_time_key, true) | ||
| 382 | if first_time then | ||
| 395 | 383 | ||
| 384 | assert(type(now_secs) == "function") | ||
| 385 | ----- | ||
| 386 | -- Snore loop (run as a lane on the background) | ||
| 396 | -- | 387 | -- |
| 397 | -- Timer lane; initialize only on the first 'require "lanes"' instance (which naturally | 388 | -- High priority, to get trustworthy timings. |
| 398 | -- has 'table' always declared) | ||
| 399 | -- | 389 | -- |
| 400 | if first_time then | 390 | -- We let the timer lane be a "free running" thread; no handle to it |
| 401 | 391 | -- remains. | |
| 402 | assert( type( now_secs) == "function") | 392 | -- |
| 403 | ----- | 393 | local timer_body = function() |
| 404 | -- Snore loop (run as a lane on the background) | 394 | set_debug_threadname("LanesTimer") |
| 405 | -- | 395 | -- |
| 406 | -- High priority, to get trustworthy timings. | 396 | -- { [deep_linda_lightuserdata]= { [deep_linda_lightuserdata]=linda_h, |
| 397 | -- [key]= { wakeup_secs [,period_secs] } [, ...] }, | ||
| 398 | -- } | ||
| 407 | -- | 399 | -- |
| 408 | -- We let the timer lane be a "free running" thread; no handle to it | 400 | -- Collection of all running timers, indexed with linda's & key. |
| 409 | -- remains. | ||
| 410 | -- | 401 | -- |
| 411 | local timer_body = function() | 402 | -- Note that we need to use the deep lightuserdata identifiers, instead |
| 412 | set_debug_threadname( "LanesTimer") | 403 | -- of 'linda_h' themselves as table indices. Otherwise, we'd get multiple |
| 413 | -- | 404 | -- entries for the same timer. |
| 414 | -- { [deep_linda_lightuserdata]= { [deep_linda_lightuserdata]=linda_h, | 405 | -- |
| 415 | -- [key]= { wakeup_secs [,period_secs] } [, ...] }, | 406 | -- The 'hidden' reference to Linda proxy is used in 'check_timers()' but |
| 416 | -- } | 407 | -- also important to keep the Linda alive, even if all outside world threw |
| 417 | -- | 408 | -- away pointers to it (which would ruin uniqueness of the deep pointer). |
| 418 | -- Collection of all running timers, indexed with linda's & key. | 409 | -- Now we're safe. |
| 419 | -- | 410 | -- |
| 420 | -- Note that we need to use the deep lightuserdata identifiers, instead | 411 | local collection = {} |
| 421 | -- of 'linda_h' themselves as table indices. Otherwise, we'd get multiple | 412 | |
| 422 | -- entries for the same timer. | 413 | local get_timers = function() |
| 423 | -- | 414 | local r = {} |
| 424 | -- The 'hidden' reference to Linda proxy is used in 'check_timers()' but | 415 | for deep, t in pairs(collection) do |
| 425 | -- also important to keep the Linda alive, even if all outside world threw | 416 | -- WR(tostring(deep)) |
| 426 | -- away pointers to it (which would ruin uniqueness of the deep pointer). | 417 | local l = t[deep] |
| 427 | -- Now we're safe. | 418 | for key, timer_data in pairs(t) do |
| 428 | -- | 419 | if key ~= deep then |
| 429 | local collection = {} | 420 | table_insert(r, {l, key, timer_data}) |
| 430 | local table_insert = assert( table.insert) | ||
| 431 | |||
| 432 | local get_timers = function() | ||
| 433 | local r = {} | ||
| 434 | for deep, t in pairs( collection) do | ||
| 435 | -- WR( tostring( deep)) | ||
| 436 | local l = t[deep] | ||
| 437 | for key, timer_data in pairs( t) do | ||
| 438 | if key ~= deep then | ||
| 439 | table_insert( r, {l, key, timer_data}) | ||
| 440 | end | ||
| 441 | end | 421 | end |
| 442 | end | 422 | end |
| 443 | return r | 423 | end |
| 444 | end -- get_timers() | 424 | return r |
| 425 | end -- get_timers() | ||
| 445 | 426 | ||
| 427 | -- | ||
| 428 | -- set_timer(linda_h, key [,wakeup_at_secs [,period_secs]] ) | ||
| 429 | -- | ||
| 430 | local set_timer = function(linda_, key_, wakeup_at_, period_) | ||
| 431 | assert(wakeup_at_ == nil or wakeup_at_ > 0.0) | ||
| 432 | assert(period_ == nil or period_ > 0.0) | ||
| 433 | |||
| 434 | local linda_deep = linda_:deep() | ||
| 435 | assert(linda_deep) | ||
| 436 | |||
| 437 | -- Find or make a lookup for this timer | ||
| 446 | -- | 438 | -- |
| 447 | -- set_timer( linda_h, key [,wakeup_at_secs [,period_secs]] ) | 439 | local t1 = collection[linda_deep] |
| 448 | -- | 440 | if not t1 then |
| 449 | local set_timer = function( linda, key, wakeup_at, period) | 441 | t1 = { [linda_deep] = linda_} -- proxy to use the Linda |
| 450 | assert( wakeup_at == nil or wakeup_at > 0.0) | 442 | collection[linda_deep] = t1 |
| 451 | assert( period == nil or period > 0.0) | 443 | end |
| 452 | 444 | ||
| 453 | local linda_deep = linda:deep() | 445 | if wakeup_at_ == nil then |
| 454 | assert( linda_deep) | 446 | -- Clear the timer |
| 447 | -- | ||
| 448 | t1[key_]= nil | ||
| 455 | 449 | ||
| 456 | -- Find or make a lookup for this timer | 450 | -- Remove empty tables from collection; speeds timer checks and |
| 451 | -- lets our 'safety reference' proxy be gc:ed as well. | ||
| 457 | -- | 452 | -- |
| 458 | local t1 = collection[linda_deep] | 453 | local empty = true |
| 459 | if not t1 then | 454 | for k, _ in pairs(t1) do |
| 460 | t1 = { [linda_deep] = linda} -- proxy to use the Linda | 455 | if k ~= linda_deep then |
| 461 | collection[linda_deep] = t1 | 456 | empty = false |
| 457 | break | ||
| 458 | end | ||
| 459 | end | ||
| 460 | if empty then | ||
| 461 | collection[linda_deep] = nil | ||
| 462 | end | 462 | end |
| 463 | 463 | ||
| 464 | if wakeup_at == nil then | 464 | -- Note: any unread timer value is left at 'linda_[key_]' intensionally; |
| 465 | -- Clear the timer | 465 | -- clearing a timer just stops it. |
| 466 | -- | 466 | else |
| 467 | t1[key]= nil | 467 | -- New timer or changing the timings |
| 468 | -- | ||
| 469 | local t2 = t1[key_] | ||
| 470 | if not t2 then | ||
| 471 | t2= {} | ||
| 472 | t1[key_]= t2 | ||
| 473 | end | ||
| 468 | 474 | ||
| 469 | -- Remove empty tables from collection; speeds timer checks and | 475 | t2[1] = wakeup_at_ |
| 470 | -- lets our 'safety reference' proxy be gc:ed as well. | 476 | t2[2] = period_ -- can be 'nil' |
| 471 | -- | 477 | end |
| 472 | local empty = true | 478 | end -- set_timer() |
| 473 | for k, _ in pairs( t1) do | ||
| 474 | if k ~= linda_deep then | ||
| 475 | empty = false | ||
| 476 | break | ||
| 477 | end | ||
| 478 | end | ||
| 479 | if empty then | ||
| 480 | collection[linda_deep] = nil | ||
| 481 | end | ||
| 482 | 479 | ||
| 483 | -- Note: any unread timer value is left at 'linda[key]' intensionally; | 480 | ----- |
| 484 | -- clearing a timer just stops it. | 481 | -- [next_wakeup_at]= check_timers() |
| 485 | else | 482 | -- Check timers, and wake up the ones expired (if any) |
| 486 | -- New timer or changing the timings | 483 | -- Returns the closest upcoming (remaining) wakeup time (or 'nil' if none). |
| 484 | local check_timers = function() | ||
| 485 | local now = now_secs() | ||
| 486 | local next_wakeup | ||
| 487 | |||
| 488 | for linda_deep,t1 in pairs(collection) do | ||
| 489 | for key,t2 in pairs(t1) do | ||
| 487 | -- | 490 | -- |
| 488 | local t2 = t1[key] | 491 | if key==linda_deep then |
| 489 | if not t2 then | 492 | -- no 'continue' in Lua :/ |
| 490 | t2= {} | 493 | else |
| 491 | t1[key]= t2 | 494 | -- 't2': { wakeup_at_secs [,period_secs] } |
| 492 | end | ||
| 493 | |||
| 494 | t2[1] = wakeup_at | ||
| 495 | t2[2] = period -- can be 'nil' | ||
| 496 | end | ||
| 497 | end -- set_timer() | ||
| 498 | |||
| 499 | ----- | ||
| 500 | -- [next_wakeup_at]= check_timers() | ||
| 501 | -- Check timers, and wake up the ones expired (if any) | ||
| 502 | -- Returns the closest upcoming (remaining) wakeup time (or 'nil' if none). | ||
| 503 | local check_timers = function() | ||
| 504 | local now = now_secs() | ||
| 505 | local next_wakeup | ||
| 506 | |||
| 507 | for linda_deep,t1 in pairs(collection) do | ||
| 508 | for key,t2 in pairs(t1) do | ||
| 509 | -- | 495 | -- |
| 510 | if key==linda_deep then | 496 | local wakeup_at= t2[1] |
| 511 | -- no 'continue' in Lua :/ | 497 | local period= t2[2] -- may be 'nil' |
| 512 | else | 498 | |
| 513 | -- 't2': { wakeup_at_secs [,period_secs] } | 499 | if wakeup_at <= now then |
| 514 | -- | 500 | local linda= t1[linda_deep] |
| 515 | local wakeup_at= t2[1] | 501 | assert(linda) |
| 516 | local period= t2[2] -- may be 'nil' | 502 | |
| 517 | 503 | linda:set(key, now ) | |
| 518 | if wakeup_at <= now then | 504 | |
| 519 | local linda= t1[linda_deep] | 505 | -- 'pairs()' allows the values to be modified (and even |
| 520 | assert(linda) | 506 | -- removed) as far as keys are not touched |
| 521 | 507 | ||
| 522 | linda:set( key, now ) | 508 | if not period then |
| 523 | 509 | -- one-time timer; gone | |
| 524 | -- 'pairs()' allows the values to be modified (and even | 510 | -- |
| 525 | -- removed) as far as keys are not touched | 511 | t1[key]= nil |
| 526 | 512 | wakeup_at= nil -- no 'continue' in Lua :/ | |
| 527 | if not period then | 513 | else |
| 528 | -- one-time timer; gone | 514 | -- repeating timer; find next wakeup (may jump multiple repeats) |
| 529 | -- | 515 | -- |
| 530 | t1[key]= nil | 516 | repeat |
| 531 | wakeup_at= nil -- no 'continue' in Lua :/ | 517 | wakeup_at= wakeup_at+period |
| 532 | else | 518 | until wakeup_at > now |
| 533 | -- repeating timer; find next wakeup (may jump multiple repeats) | 519 | |
| 534 | -- | 520 | t2[1]= wakeup_at |
| 535 | repeat | ||
| 536 | wakeup_at= wakeup_at+period | ||
| 537 | until wakeup_at > now | ||
| 538 | |||
| 539 | t2[1]= wakeup_at | ||
| 540 | end | ||
| 541 | end | ||
| 542 | |||
| 543 | if wakeup_at and ((not next_wakeup) or (wakeup_at < next_wakeup)) then | ||
| 544 | next_wakeup= wakeup_at | ||
| 545 | end | 521 | end |
| 546 | end | 522 | end |
| 547 | end -- t2 loop | ||
| 548 | end -- t1 loop | ||
| 549 | |||
| 550 | return next_wakeup -- may be 'nil' | ||
| 551 | end -- check_timers() | ||
| 552 | |||
| 553 | local timer_gateway_batched = timer_gateway.batched | ||
| 554 | set_finalizer( function( err, stk) | ||
| 555 | if err and type( err) ~= "userdata" then | ||
| 556 | WR( "LanesTimer error: "..tostring(err)) | ||
| 557 | --elseif type( err) == "userdata" then | ||
| 558 | -- WR( "LanesTimer after cancel" ) | ||
| 559 | --else | ||
| 560 | -- WR("LanesTimer finalized") | ||
| 561 | end | ||
| 562 | end) | ||
| 563 | while true do | ||
| 564 | local next_wakeup = check_timers() | ||
| 565 | 523 | ||
| 566 | -- Sleep until next timer to wake up, or a set/clear command | 524 | if wakeup_at and ((not next_wakeup) or (wakeup_at < next_wakeup)) then |
| 567 | -- | 525 | next_wakeup= wakeup_at |
| 568 | local secs | 526 | end |
| 569 | if next_wakeup then | ||
| 570 | secs = next_wakeup - now_secs() | ||
| 571 | if secs < 0 then secs = 0 end | ||
| 572 | end | ||
| 573 | local key, what = timer_gateway:receive( secs, TGW_KEY, TGW_QUERY) | ||
| 574 | |||
| 575 | if key == TGW_KEY then | ||
| 576 | assert( getmetatable( what) == "Linda") -- 'what' should be a linda on which the client sets a timer | ||
| 577 | local _, key, wakeup_at, period = timer_gateway:receive( 0, timer_gateway_batched, TGW_KEY, 3) | ||
| 578 | assert( key) | ||
| 579 | set_timer( what, key, wakeup_at, period and period > 0 and period or nil) | ||
| 580 | elseif key == TGW_QUERY then | ||
| 581 | if what == "get_timers" then | ||
| 582 | timer_gateway:send( TGW_REPLY, get_timers()) | ||
| 583 | else | ||
| 584 | timer_gateway:send( TGW_REPLY, "unknown query " .. what) | ||
| 585 | end | 527 | end |
| 586 | --elseif secs == nil then -- got no value while block-waiting? | 528 | end -- t2 loop |
| 587 | -- WR( "timer lane: no linda, aborted?") | 529 | end -- t1 loop |
| 588 | end | 530 | |
| 531 | return next_wakeup -- may be 'nil' | ||
| 532 | end -- check_timers() | ||
| 533 | |||
| 534 | local timer_gateway_batched = timer_gateway.batched | ||
| 535 | set_finalizer(function(err, stk) | ||
| 536 | if err and type(err) ~= "userdata" then | ||
| 537 | WR("LanesTimer error: "..tostring(err)) | ||
| 538 | --elseif type(err) == "userdata" then | ||
| 539 | -- WR("LanesTimer after cancel" ) | ||
| 540 | --else | ||
| 541 | -- WR("LanesTimer finalized") | ||
| 589 | end | 542 | end |
| 590 | end -- timer_body() | 543 | end) |
| 591 | timer_lane = gen( "*", { package= {}, priority = max_prio}, timer_body)() -- "*" instead of "io,package" for LuaJIT compatibility... | 544 | while true do |
| 592 | end -- first_time | 545 | local next_wakeup = check_timers() |
| 593 | 546 | ||
| 594 | ----- | 547 | -- Sleep until next timer to wake up, or a set/clear command |
| 595 | -- = timer( linda_h, key_val, date_tbl|first_secs [,period_secs] ) | ||
| 596 | -- | ||
| 597 | -- PUBLIC LANES API | ||
| 598 | timer = function( linda, key, a, period ) | ||
| 599 | if getmetatable( linda) ~= "Linda" then | ||
| 600 | error "expecting a Linda" | ||
| 601 | end | ||
| 602 | if a == 0.0 then | ||
| 603 | -- Caller expects to get current time stamp in Linda, on return | ||
| 604 | -- (like the timer had expired instantly); it would be good to set this | ||
| 605 | -- as late as possible (to give most current time) but also we want it | ||
| 606 | -- to precede any possible timers that might start striking. | ||
| 607 | -- | 548 | -- |
| 608 | linda:set( key, now_secs()) | 549 | local secs |
| 609 | 550 | if next_wakeup then | |
| 610 | if not period or period==0.0 then | 551 | secs = next_wakeup - now_secs() |
| 611 | timer_gateway:send( TGW_KEY, linda, key, nil, nil ) -- clear the timer | 552 | if secs < 0 then secs = 0 end |
| 612 | return -- nothing more to do | 553 | end |
| 554 | local key, what = timer_gateway:receive(secs, TGW_KEY, TGW_QUERY) | ||
| 555 | |||
| 556 | if key == TGW_KEY then | ||
| 557 | assert(getmetatable(what) == "Linda") -- 'what' should be a linda on which the client sets a timer | ||
| 558 | local _, key, wakeup_at, period = timer_gateway:receive(0, timer_gateway_batched, TGW_KEY, 3) | ||
| 559 | assert(key) | ||
| 560 | set_timer(what, key, wakeup_at, period and period > 0 and period or nil) | ||
| 561 | elseif key == TGW_QUERY then | ||
| 562 | if what == "get_timers" then | ||
| 563 | timer_gateway:send(TGW_REPLY, get_timers()) | ||
| 564 | else | ||
| 565 | timer_gateway:send(TGW_REPLY, "unknown query " .. what) | ||
| 566 | end | ||
| 567 | --elseif secs == nil then -- got no value while block-waiting? | ||
| 568 | -- WR("timer lane: no linda, aborted?") | ||
| 613 | end | 569 | end |
| 614 | a= period | ||
| 615 | end | 570 | end |
| 571 | end -- timer_body() | ||
| 572 | timer_lane = gen("*", { package= {}, priority = core.max_prio}, timer_body)() -- "*" instead of "io,package" for LuaJIT compatibility... | ||
| 573 | end -- first_time | ||
| 616 | 574 | ||
| 617 | local wakeup_at= type(a)=="table" and wakeup_conv(a) -- given point of time | 575 | ----- |
| 618 | or (a and now_secs()+a or nil) | 576 | -- = timer(linda_h, key_val, date_tbl|first_secs [,period_secs] ) |
| 619 | -- queue to timer | 577 | -- |
| 578 | -- PUBLIC LANES API | ||
| 579 | timer = function(linda_, key_, when_, period_) | ||
| 580 | if getmetatable(linda_) ~= "Linda" then | ||
| 581 | error "expecting a Linda" | ||
| 582 | end | ||
| 583 | if when_ == 0.0 then | ||
| 584 | -- Caller expects to get current time stamp in Linda, on return | ||
| 585 | -- (like the timer had expired instantly); it would be good to set this | ||
| 586 | -- as late as possible (to give most current time) but also we want it | ||
| 587 | -- to precede any possible timers that might start striking. | ||
| 620 | -- | 588 | -- |
| 621 | timer_gateway:send( TGW_KEY, linda, key, wakeup_at, period ) | 589 | linda_:set(key_, now_secs()) |
| 622 | end -- timer() | ||
| 623 | 590 | ||
| 624 | ----- | 591 | if not period_ or period_ == 0.0 then |
| 625 | -- {[{linda, slot, when, period}[,...]]} = timers() | 592 | timer_gateway:send(TGW_KEY, linda_, key_, nil, nil ) -- clear the timer |
| 593 | return -- nothing more to do | ||
| 594 | end | ||
| 595 | when_ = period_ | ||
| 596 | end | ||
| 597 | |||
| 598 | local wakeup_at = type(when_)=="table" and wakeup_conv(when_) -- given point of time | ||
| 599 | or (when_ and now_secs()+when_ or nil) | ||
| 600 | -- queue to timer | ||
| 626 | -- | 601 | -- |
| 627 | -- PUBLIC LANES API | 602 | timer_gateway:send(TGW_KEY, linda_, key_, wakeup_at, period_) |
| 628 | timers = function() | 603 | end -- timer() |
| 629 | timer_gateway:send( TGW_QUERY, "get_timers") | ||
| 630 | local _, r = timer_gateway:receive( TGW_REPLY) | ||
| 631 | return r | ||
| 632 | end -- timers() | ||
| 633 | 604 | ||
| 634 | end -- settings.with_timers | 605 | ----- |
| 606 | -- {[{linda, slot, when, period}[,...]]} = timers() | ||
| 607 | -- | ||
| 608 | -- PUBLIC LANES API | ||
| 609 | timers = function() | ||
| 610 | timer_gateway:send(TGW_QUERY, "get_timers") | ||
| 611 | local _, r = timer_gateway:receive(TGW_REPLY) | ||
| 612 | return r | ||
| 613 | end -- timers() | ||
| 614 | end | ||
| 635 | 615 | ||
| 636 | -- avoid pulling the whole core module as upvalue when cancel_error is enough | 616 | -- ################################################################################################# |
| 637 | local cancel_error = assert( core.cancel_error) | 617 | -- ###################################### lanes.sleep() ############################################ |
| 618 | -- ################################################################################################# | ||
| 638 | 619 | ||
| 639 | ---=== Lock & atomic generators ===--- | 620 | -- <void> = sleep([seconds_]) |
| 621 | -- | ||
| 622 | -- PUBLIC LANES API | ||
| 623 | local sleep = function(seconds_) | ||
| 624 | seconds_ = seconds_ or 0.0 -- this causes false and nil to be a valid input, equivalent to 0.0, but that's ok | ||
| 625 | if type(seconds_) ~= "number" then | ||
| 626 | error("invalid duration " .. string_format("%q", tostring(seconds_))) | ||
| 627 | end | ||
| 628 | -- receive data on a channel no-one ever sends anything, thus blocking for the specified duration | ||
| 629 | return timer_gateway:receive(seconds_, "ac100de1-a696-4619-b2f0-a26de9d58ab8") | ||
| 630 | end -- sleep | ||
| 640 | 631 | ||
| 632 | -- ################################################################################################# | ||
| 633 | -- ##################################### lanes.genlock() ########################################### | ||
| 634 | -- ################################################################################################# | ||
| 641 | -- These functions are just surface sugar, but make solutions easier to read. | 635 | -- These functions are just surface sugar, but make solutions easier to read. |
| 642 | -- Not many applications should even need explicit locks or atomic counters. | 636 | -- Not many applications should even need explicit locks or atomic counters. |
| 643 | 637 | ||
| 644 | -- | 638 | -- |
| 645 | -- [true [, ...]= trues(uint) | 639 | -- [true [, ...]= trues(uint) |
| 646 | -- | 640 | -- |
| 647 | local function trues( n) | 641 | local function trues(n) |
| 648 | if n > 0 then | 642 | if n > 0 then |
| 649 | return true, trues( n - 1) | 643 | return true, trues(n - 1) |
| 650 | end | ||
| 651 | end | 644 | end |
| 645 | end | ||
| 652 | 646 | ||
| 653 | -- | 647 | -- avoid pulling the whole core module as upvalue when cancel_error is enough |
| 654 | -- lock_f = lanes.genlock( linda_h, key [,N_uint=1] ) | 648 | local cancel_error |
| 655 | -- | 649 | |
| 656 | -- = lock_f( +M ) -- acquire M | 650 | -- ################################################################################################# |
| 657 | -- ...locked... | 651 | |
| 658 | -- = lock_f( -M ) -- release M | 652 | -- lock_f = lanes.genlock(linda_h, key [,N_uint=1] ) |
| 659 | -- | 653 | -- |
| 660 | -- Returns an access function that allows 'N' simultaneous entries between | 654 | -- = lock_f(+M) -- acquire M |
| 661 | -- acquire (+M) and release (-M). For binary locks, use M==1. | 655 | -- ...locked... |
| 662 | -- | 656 | -- = lock_f(-M) -- release M |
| 663 | -- PUBLIC LANES API | 657 | -- |
| 664 | local genlock = function( linda, key, N) | 658 | -- Returns an access function that allows 'N' simultaneous entries between |
| 665 | -- clear existing data and set the limit | 659 | -- acquire (+M) and release (-M). For binary locks, use M==1. |
| 666 | N = N or 1 | 660 | -- |
| 667 | if linda:set( key) == cancel_error or linda:limit( key, N) == cancel_error then | 661 | local genlock = function(linda_, key_, N) |
| 668 | return cancel_error | 662 | -- clear existing data and set the limit |
| 663 | N = N or 1 | ||
| 664 | if linda_:set(key_) == cancel_error or linda_:limit(key_, N) == cancel_error then | ||
| 665 | return cancel_error | ||
| 666 | end | ||
| 667 | |||
| 668 | -- use an optimized version for case N == 1 | ||
| 669 | return (N == 1) and | ||
| 670 | function(M_, mode_) | ||
| 671 | local timeout = (mode_ == "try") and 0 or nil | ||
| 672 | if M_ > 0 then | ||
| 673 | -- 'nil' timeout allows 'key_' to be numeric | ||
| 674 | return linda_:send(timeout, key_, true) -- suspends until been able to push them | ||
| 675 | else | ||
| 676 | local k = linda_:receive(nil, key_) | ||
| 677 | -- propagate cancel_error if we got it, else return true or false | ||
| 678 | return k and ((k ~= cancel_error) and true or k) or false | ||
| 669 | end | 679 | end |
| 680 | end | ||
| 681 | or | ||
| 682 | function(M_, mode_) | ||
| 683 | local timeout = (mode_ == "try") and 0 or nil | ||
| 684 | if M_ > 0 then | ||
| 685 | -- 'nil' timeout allows 'key_' to be numeric | ||
| 686 | return linda_:send(timeout, key_, trues(M_)) -- suspends until been able to push them | ||
| 687 | else | ||
| 688 | local k = linda_:receive(nil, linda_.batched, key_, -M_) | ||
| 689 | -- propagate cancel_error if we got it, else return true or false | ||
| 690 | return k and ((k ~= cancel_error) and true or k) or false | ||
| 691 | end | ||
| 692 | end | ||
| 693 | end -- genlock | ||
| 670 | 694 | ||
| 671 | -- use an optimized version for case N == 1 | 695 | -- ################################################################################################# |
| 672 | return (N == 1) and | 696 | -- #################################### lanes.genatomic() ########################################## |
| 673 | function( M, mode_) | 697 | -- ################################################################################################# |
| 674 | local timeout = (mode_ == "try") and 0 or nil | 698 | |
| 675 | if M > 0 then | 699 | -- atomic_f = lanes.genatomic(linda_h, key [,initial_num=0.0]) |
| 676 | -- 'nil' timeout allows 'key' to be numeric | 700 | -- |
| 677 | return linda:send( timeout, key, true) -- suspends until been able to push them | 701 | -- int|cancel_error = atomic_f([diff_num = 1.0]) |
| 678 | else | 702 | -- |
| 679 | local k = linda:receive( nil, key) | 703 | -- Returns an access function that allows atomic increment/decrement of the |
| 680 | -- propagate cancel_error if we got it, else return true or false | 704 | -- number in 'key'. |
| 681 | return k and ((k ~= cancel_error) and true or k) or false | 705 | -- |
| 682 | end | 706 | local genatomic = function(linda_, key_, initial_val_) |
| 707 | -- clears existing data (also queue). the slot may contain the stored value, and an additional boolean value | ||
| 708 | if linda_:limit(key_, 2) == cancel_error or linda_:set(key_, initial_val_ or 0.0) == cancel_error then | ||
| 709 | return cancel_error | ||
| 710 | end | ||
| 711 | |||
| 712 | return function(diff_) | ||
| 713 | -- 'nil' allows 'key_' to be numeric | ||
| 714 | -- suspends until our 'true' is in | ||
| 715 | if linda_:send(nil, key_, true) == cancel_error then | ||
| 716 | return cancel_error | ||
| 683 | end | 717 | end |
| 684 | or | 718 | local val = linda_:get(key_) |
| 685 | function( M, mode_) | 719 | if val ~= cancel_error then |
| 686 | local timeout = (mode_ == "try") and 0 or nil | 720 | val = val + (diff_ or 1.0) |
| 687 | if M > 0 then | 721 | -- set() releases the lock by emptying queue |
| 688 | -- 'nil' timeout allows 'key' to be numeric | 722 | if linda_:set(key_, val) == cancel_error then |
| 689 | return linda:send( timeout, key, trues(M)) -- suspends until been able to push them | 723 | val = cancel_error |
| 690 | else | ||
| 691 | local k = linda:receive( nil, linda.batched, key, -M) | ||
| 692 | -- propagate cancel_error if we got it, else return true or false | ||
| 693 | return k and ((k ~= cancel_error) and true or k) or false | ||
| 694 | end | 724 | end |
| 695 | end | 725 | end |
| 726 | return val | ||
| 696 | end | 727 | end |
| 728 | end -- genatomic | ||
| 697 | 729 | ||
| 730 | -- ################################################################################################# | ||
| 731 | -- ################################## lanes.configure() ############################################ | ||
| 732 | -- ################################################################################################# | ||
| 698 | 733 | ||
| 734 | -- this function is available in the public interface until it is called, after which it disappears | ||
| 735 | lanes.configure = function(settings_) | ||
| 736 | |||
| 737 | -- This check is for sublanes requiring Lanes | ||
| 699 | -- | 738 | -- |
| 700 | -- atomic_f = lanes.genatomic( linda_h, key [,initial_num=0.0]) | 739 | -- TBD: We could also have the C level expose 'string.gmatch' for us. But this is simpler. |
| 701 | -- | ||
| 702 | -- int|cancel_error = atomic_f( [diff_num = 1.0]) | ||
| 703 | -- | 740 | -- |
| 704 | -- Returns an access function that allows atomic increment/decrement of the | 741 | if not string then |
| 705 | -- number in 'key'. | 742 | error("To use 'lanes', you will also need to have 'string' available.", 2) |
| 743 | end | ||
| 744 | -- Configure called so remove metatable from lanes | ||
| 745 | setmetatable(lanes, nil) | ||
| 746 | |||
| 747 | -- now we can configure Lanes core | ||
| 748 | local settings = core.configure and core.configure(params_checker(settings_)) or core.settings | ||
| 749 | |||
| 706 | -- | 750 | -- |
| 707 | -- PUBLIC LANES API | 751 | lanes.ABOUT = |
| 708 | local genatomic = function( linda, key, initial_val) | 752 | { |
| 709 | -- clears existing data (also queue). the slot may contain the stored value, and an additional boolean value | 753 | author= "Asko Kauppi <akauppi@gmail.com>, Benoit Germain <bnt.germain@gmail.com>", |
| 710 | if linda:limit( key, 2) == cancel_error or linda:set( key, initial_val or 0.0) == cancel_error then | 754 | description= "Running multiple Lua states in parallel", |
| 711 | return cancel_error | 755 | license= "MIT/X11", |
| 712 | end | 756 | copyright= "Copyright (c) 2007-10, Asko Kauppi; (c) 2011-23, Benoit Germain", |
| 757 | version = assert(core.version) | ||
| 758 | } | ||
| 713 | 759 | ||
| 714 | return function( diff) | 760 | -- avoid pulling the whole core module as upvalue when cancel_error is enough |
| 715 | -- 'nil' allows 'key' to be numeric | 761 | -- these are locals declared above, that we need to set prior to calling configure_timers() |
| 716 | -- suspends until our 'true' is in | 762 | cancel_error = assert(core.cancel_error) |
| 717 | if linda:send( nil, key, true) == cancel_error then | 763 | timer_gateway = assert(core.timer_gateway) |
| 718 | return cancel_error | 764 | |
| 719 | end | 765 | if settings.with_timers then |
| 720 | local val = linda:get( key) | 766 | configure_timers(settings) |
| 721 | if val ~= cancel_error then | ||
| 722 | val = val + (diff or 1.0) | ||
| 723 | -- set() releases the lock by emptying queue | ||
| 724 | if linda:set( key, val) == cancel_error then | ||
| 725 | val = cancel_error | ||
| 726 | end | ||
| 727 | end | ||
| 728 | return val | ||
| 729 | end | ||
| 730 | end | 767 | end |
| 731 | 768 | ||
| 732 | -- activate full interface | 769 | -- activate full interface |
| 733 | lanes.require = core.require | ||
| 734 | lanes.register = core.register | ||
| 735 | lanes.gen = gen | ||
| 736 | lanes.linda = core.linda | ||
| 737 | lanes.cancel_error = core.cancel_error | 770 | lanes.cancel_error = core.cancel_error |
| 771 | lanes.linda = core.linda | ||
| 738 | lanes.nameof = core.nameof | 772 | lanes.nameof = core.nameof |
| 773 | lanes.now_secs = core.now_secs | ||
| 774 | lanes.require = core.require | ||
| 775 | lanes.register = core.register | ||
| 739 | lanes.set_singlethreaded = core.set_singlethreaded | 776 | lanes.set_singlethreaded = core.set_singlethreaded |
| 740 | lanes.threads = core.threads or function() error "lane tracking is not available" end -- core.threads isn't registered if settings.track_lanes is false | ||
| 741 | lanes.set_thread_priority = core.set_thread_priority | ||
| 742 | lanes.set_thread_affinity = core.set_thread_affinity | 777 | lanes.set_thread_affinity = core.set_thread_affinity |
| 778 | lanes.set_thread_priority = core.set_thread_priority | ||
| 779 | lanes.threads = core.threads or function() error "lane tracking is not available" end -- core.threads isn't registered if settings.track_lanes is false | ||
| 780 | |||
| 781 | lanes.gen = gen | ||
| 782 | lanes.genatomic = genatomic | ||
| 783 | lanes.genlock = genlock | ||
| 784 | lanes.sleep = sleep | ||
| 743 | lanes.timer = timer | 785 | lanes.timer = timer |
| 744 | lanes.timer_lane = timer_lane | 786 | lanes.timer_lane = timer_lane |
| 745 | lanes.timers = timers | 787 | lanes.timers = timers |
| 746 | lanes.sleep = sleep | ||
| 747 | lanes.genlock = genlock | ||
| 748 | lanes.now_secs = core.now_secs | ||
| 749 | lanes.genatomic = genatomic | ||
| 750 | lanes.configure = nil -- no need to call configure() ever again | 788 | lanes.configure = nil -- no need to call configure() ever again |
| 751 | return lanes | 789 | return lanes |
| 752 | end -- lanes.configure | 790 | end -- lanes.configure |
| 753 | 791 | ||
| 754 | lanesMeta.__index = function( t, k) | 792 | -- ################################################################################################# |
| 793 | |||
| 794 | lanesMeta.__index = function(_, k_) | ||
| 755 | -- This is called when some functionality is accessed without calling configure() | 795 | -- This is called when some functionality is accessed without calling configure() |
| 756 | lanes.configure() -- initialize with default settings | 796 | lanes.configure() -- initialize with default settings |
| 757 | -- Access the required key | 797 | -- Access the required key |
| 758 | return lanes[k] | 798 | return lanes[k_] |
| 759 | end | 799 | end |
| 760 | 800 | ||
| 801 | -- ################################################################################################# | ||
| 802 | |||
| 761 | -- no need to force calling configure() manually excepted the first time (other times will reuse the internally stored settings of the first call) | 803 | -- no need to force calling configure() manually excepted the first time (other times will reuse the internally stored settings of the first call) |
| 762 | if core.settings then | 804 | if core.settings then |
| 763 | return lanes.configure() | 805 | return lanes.configure() |
