├── .gitignore ├── .luacov ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── echo-server.lua ├── pingpong.lua └── timeout.lua ├── luactor.lua ├── luactor ├── config.lua └── reactor │ ├── luaevent.lua │ ├── reactor_template.lua │ └── uloop.lua └── tests ├── report_coverage.lua ├── run_tests.sh ├── setup.sh └── test_echo-server.py /.gitignore: -------------------------------------------------------------------------------- 1 | tests/luacov.*.* 2 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | --- Global configuration file. Copy, customize and store in your 2 | -- project folder as '.luacov' for project specific configuration 3 | -- @class module 4 | -- @name luacov.defaults 5 | return { 6 | 7 | -- default filename to load for config options if not provided 8 | -- only has effect in 'luacov.defaults.lua' 9 | ["configfile"] = ".luacov", 10 | 11 | -- filename to store stats collected 12 | ["statsfile"] = "tests/luacov.stats.out", 13 | 14 | -- filename to store report 15 | ["reportfile"] = "tests/luacov.report.json", 16 | 17 | -- Run reporter on completion? (won't work for ticks) 18 | runreport = false, 19 | 20 | -- Delete stats file after reporting? 21 | deletestats = false, 22 | 23 | -- Patterns for files to include when reporting 24 | -- all will be included if nothing is listed 25 | -- (exclude overrules include, do not include 26 | -- the .lua extension, path separator is always '/') 27 | ["include"] = { 28 | "example/.*$", 29 | "reactor/.*$", 30 | "^./.*$", 31 | }, 32 | 33 | -- Patterns for files to exclude when reporting 34 | -- all will be included if nothing is listed 35 | -- (exclude overrules include, do not include 36 | -- the .lua extension, path separator is always '/') 37 | ["exclude"] = { 38 | "util$", 39 | "luacov$", 40 | "luacov/reporter$", 41 | "luacov/defaults$", 42 | "luacov/runner$", 43 | "luacov/stats$", 44 | "luacov/tick$", 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | env: 4 | matrix: 5 | - "LUA=lua5.1 LUACTOR_REACTOR=luaevent" 6 | - "LUA=lua5.1 LUACTOR_REACTOR=uloop" 7 | 8 | install: 9 | - "source tests/setup.sh" 10 | 11 | script: 12 | - "source tests/run_tests.sh" 13 | 14 | after_success: 15 | - "$LUA tests/report_coverage.lua" 16 | - 'curl -v -H "Content-Type: multipart/related" --form "json_file=@tests/luacov.report.json;type=application/json" https://coveralls.io/api/v1/jobs' 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Credo Semiconductor (Shanghai) Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LuActor - Actor Model for Lua 2 | ============================= 3 | 4 | [![Build Status](https://travis-ci.org/xfguo/luactor.svg?branch=master)](https://travis-ci.org/xfguo/luactor) [![Coverage Status](https://coveralls.io/repos/xfguo/luactor/badge.png?branch=master)](https://coveralls.io/r/xfguo/luactor?branch=master) 5 | 6 | A pure Lua (at least for now) [Actor Model](http://en.wikipedia.org/wiki/Actor_model) framework. 7 | 8 | **Luactor** is based on **coroutine** in Lua. 9 | 10 | Inspired by these projects and articles, thanks for all of them. 11 | 12 | - [@cloudwu](https://github.com/cloudwu)'s [skynet](https://github.com/cloudwu/skynet) and his [blog](http://blog.codingnow.com). 13 | - [gevent](http://www.gevent.org/) 14 | - [ratchet](https://github.com/icgood/ratchet) 15 | 16 | License 17 | ------- 18 | 19 | Licensed under the Apache License, Version 2.0. See *LICENSE*. 20 | 21 | Dependencies 22 | ------------ 23 | 24 | - Lua 5.1 25 | - A reactor library. (See *Reactor/Dispatcher Driver*) 26 | 27 | *(I don't test it but Lua 5.2 might work.)* 28 | 29 | ### Reactor/Dispatcher Driver 30 | 31 | **LuActor** use a [**reactor**](http://en.wikipedia.org/wiki/Reactor_pattern) to monitor all external events (Timeout/FD/...). 32 | 33 | For now, it support: 34 | 35 | - [luaevent](https://github.com/harningt/luaevent) 36 | - luaevent v0.4.3 has been tested, older verison may have some issues (At least for v0.3.2 on Ubuntu 12.04). So please make sure you have the right version of luaevent. 37 | - [libubox:uloop](https://github.com/xfguo/libubox) 38 | - NOTE: **uloop** is a part of libubox which is design for OpenWrt(a GLib like library). For now, it just test with the modified version of libubox. Since the lua binding of uloop is missing the fd operation part. 39 | 40 | `reactor/reactor_template.lua` is a template of the reactor library. If you are using other event-driven library(eg. lua-ev/uloop(OpenWrt)/..., or epoll/kqueue/...), you just need implement the methods in this file. 41 | 42 | **Any other patch or pull request of a reactor implementation is welcome.** 43 | 44 | How to Use 45 | ---------- 46 | 47 | ### Summary 48 | 49 | All the methods are contained within a lua *table* (like *coroutine*). 50 | 51 | The methods include: 52 | 53 | - `create` - create an actor 54 | - `start` - start an actor 55 | - `register_event` - register an event 56 | - `unregister_event` - unregister an event 57 | - `send` - send a message to another actor 58 | - `wait` - wait a message 59 | - `run` - run 60 | 61 | ### Loading the library 62 | 63 | Before you use **luactor**, you should set the environment variable `LUACTOR_REACTOR` to `luaevent` or `uloop` to choose the **reactor**. If `LUACTOR_REACTOR` is not set, the default **rector** is `luaevent` for now. 64 | 65 | local actor = require "luactor" 66 | 67 | ### Create a new actor 68 | 69 | To create a new actor: 70 | 71 | new_actor_obj = actor.create(name, f) 72 | 73 | - `name` is the actor's name, and is unique, which is for receiving message from other actor. 74 | - `f` is the Lua function. 75 | 76 | ### Start the actor 77 | 78 | After you create an actor, you can just start it. 79 | 80 | actor.start(actor_obj, ...) 81 | 82 | - `actor_obj` is the return value when you run `actor.create`. 83 | - `...` is the arguments you want to pass to the function when you registered it. 84 | 85 | ### Send a message to another actor **unblocked** 86 | 87 | To send a message, a lua object of any type, to another actor. 88 | 89 | actor.send(receiver, command, message) 90 | 91 | - `receiver` is the name of an actor when it created. 92 | - `command` is a string for declare a message type. 93 | - `message` is the message object you want to send to *receiver*. It could be any type of lua object, include nil. 94 | 95 | ### Wait and handle a message **blocked** 96 | 97 | Use `actor.wait` like: 98 | 99 | actor.wait({ 100 | command1 = 101 | function (message, sender) 102 | -- handle message 103 | end, 104 | [ 105 | command2 = 106 | function (message, sender) 107 | end, 108 | ... 109 | ] 110 | }) 111 | 112 | `actor.wait` accept a Lua table which the *key* is the command it want to receive and the *value* is a Lua function to process the message. 113 | 114 | 115 | 116 | ### Register and Unregister an Event 117 | 118 | An *actor* can register a fd/timeout event, so when that event is triggered. **luactor** will send a message to the *actor*. The `command` of the message is `fd` or `timeout`. 119 | 120 | To register an event: 121 | 122 | actor.register(ev_type, ev_name, ...) 123 | 124 | - `ev_type` is the type of the event. For now, **luactor** support `fd` or `timeout`. 125 | - `ev_name` is the unique name of this event in the actor. if the event triggerd, *luactor* will send a message to the actor which the command is the *ev_name*. 126 | - `...` is the arguments of event register. 127 | - For `fd` event, it accept `fd` and `event` two arguments. `fd` is the file descriptor. `event` is the event type you want to trigger, can be `'read'` or `'write'`. 128 | - For `timeout` event, only one argument `timeout` is needed. 129 | 130 | To unregister the event: 131 | 132 | actor.unregister(ev_name) 133 | 134 | - `ev_name` is the unique name when registered. 135 | 136 | ### Start running 137 | 138 | To let all actors start running, just: 139 | 140 | actor.run() 141 | 142 | This method will be blocked until all the actors exited. 143 | 144 | Examples 145 | -------- 146 | 147 | Make sure you set the environment variable `LUACTOR_REACTOR` to choose the **reactor**, see chapter **Loading the library**. 148 | 149 | ### Ping-Pong 150 | 151 | Two actors **ping** and **pong** receive and send message to each other. 152 | 153 | lua example/pingpang.lua 154 | 155 | ### Timeout 156 | 157 | An actor register a **timeout** event, then wait a timeout message. 158 | 159 | lua example/timeout.lua 160 | 161 | ### Echo Server (TCP) 162 | 163 | An actor **tcp_manager** responses to listen TCP socket, when a new connection is 164 | established, create a new **echo_actor** and let it handle that connection. 165 | 166 | Run echo server: 167 | 168 | lua example/echo-server.lua 169 | 170 | Then, run: 171 | 172 | telnet 127.0.0.1 8080 173 | 174 | You can open multiple telnet to test it. 175 | 176 | Some operations for echo server. 177 | - If `exit` is send to echo server, all actors will **exit**. 178 | - If `raise` is send to echo server, the actor will raise an error but **tcp_manager** will handle it. 179 | 180 | Author 181 | ------ 182 | 183 | - Xiongfei Guo 184 | -------------------------------------------------------------------------------- /example/echo-server.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- TCP Echo Server Example 3 | -- 4 | -- Reference luasocket example : 5 | -- http://w3.impa.br/~diego/software/luasocket/introduction.html 6 | -- 7 | 8 | local actor = require "luactor" 9 | local socket = require("socket") 10 | 11 | -- Actors --------------------------------------------------------------------- 12 | 13 | -- 14 | -- EchoActor 15 | -- 16 | -- handle a TCP connection, echo back everything 17 | -- 18 | local echo_actor_func = function(conn, name) 19 | print(string.format('EchoActor[%s] start...', name)) 20 | 21 | -- register fd event for new data coming 22 | actor.register_event( 23 | 'fd', -- event type 24 | 'new_conn', -- event name 25 | conn, -- fd want to listen 26 | 'read' -- fd event type 27 | ) 28 | 29 | local finish = false 30 | while not finish do 31 | -- listen fd event 32 | actor.wait({ 33 | -- wait fd event 34 | new_conn = function (msg) 35 | -- receive it 36 | local line, err = conn:receive() 37 | if not err then 38 | -- echo back 39 | conn:send(line .. "\n") 40 | 41 | -- if got a 'exit' command, send exit message 42 | -- to tcp_manager. later, we will receive a 43 | -- exit event. 44 | if line == "exit" then 45 | actor.send('tcp_manager', 'exit') 46 | elseif line == "raise" then 47 | error("raise error") 48 | end 49 | else 50 | -- connection close 51 | -- send a message to delete my info 52 | actor.send('tcp_manager', 'echo_actor_finished') 53 | finish = true 54 | end 55 | end, 56 | 57 | -- wait an exit message 58 | exit = function (msg) 59 | finish = true 60 | print(string.format('EchoActor[%s] got a exit command.', name)) 61 | end, 62 | }) 63 | end 64 | 65 | -- unregister fd event, close and exit 66 | actor.unregister_event('new_conn') 67 | conn:close() 68 | 69 | print(string.format('EchoActor[%s] end...', name)) 70 | end 71 | 72 | -- 73 | -- TcpManager 74 | -- 75 | -- listen a TCP socket, when new TCP connection established, pass it to a new EchoActor 76 | -- 77 | tcp_manager_func = function () 78 | local active_echo_actors = {} 79 | local accepted_conns = {} 80 | local conn_no = 0 81 | 82 | print('TcpManager start...') 83 | print("Use `telnet localhost 48888` to test it") 84 | 85 | local server = assert(socket.bind("*", 48888)) 86 | 87 | -- register fd event for accept new connection 88 | actor.register_event( 89 | 'fd', -- event type 90 | 'accept_conn', -- event name 91 | server, -- fd want to listen 92 | 'read' -- fd event type 93 | ) 94 | 95 | local finish = false 96 | while not finish do 97 | -- listen fd event 98 | actor.wait({ 99 | -- wait a fd event 100 | accept_conn = function (msg) 101 | local conn = server:accept() 102 | 103 | -- generate a new name 104 | local conn_name = 'tcp_conn_'..conn_no 105 | conn_no = conn_no + 1 106 | 107 | -- create a new EchoActor by send a message to scheduler 108 | local new_echo_actor = actor.create( 109 | conn_name, -- new actor's name 110 | echo_actor_func -- actor function 111 | ) 112 | 113 | -- start the new echo actor 114 | actor.start(new_echo_actor, conn, conn_name) 115 | 116 | -- attach the name of echo actor to the pool 117 | active_echo_actors[conn_name] = true 118 | 119 | -- hold the accepted connections for error handling 120 | accepted_conns[conn_name] = conn 121 | end, 122 | 123 | -- wait finish message from echo actors 124 | echo_actor_finished = function (msg, sender) 125 | active_echo_actors[sender] = nil 126 | accepted_conns[sender] = nil 127 | end, 128 | 129 | -- handle the error of echo actor 130 | -- close the connection and unregister the related events. 131 | actor_error = function (msg) 132 | local failed_echo_actor_name = msg.actor.name 133 | accepted_conns[failed_echo_actor_name]:close() 134 | print(string.format( 135 | 'Echo actor: [%s] failed: \ntrace:\n\t%s\nerror: %s', 136 | failed_echo_actor_name, 137 | string.gsub(msg.error[1], '\n', '\n\t'), 138 | msg.error[2] 139 | )) 140 | end, 141 | 142 | -- wait an exit message sent from echo actor 143 | exit = function (msg, sender) 144 | print('Got Exit Message from '..sender) 145 | print('Send `exit` to all echo actors') 146 | for echo_actor_name, _ in pairs(active_echo_actors) do 147 | actor.send(echo_actor_name, 'exit') 148 | end 149 | finish = true 150 | end, 151 | }) 152 | end 153 | 154 | actor.unregister_event('accept_conn') 155 | server:close() 156 | print('TcpManager end...') 157 | end 158 | 159 | -- Main ----------------------------------------------------------------------- 160 | local reactor_name = os.getenv("LUACTOR_REACTOR") or 'luaevent' 161 | print('The reactor you use is *'..reactor_name..'*.') 162 | 163 | tcp_manager = actor.create('tcp_manager', tcp_manager_func) 164 | 165 | actor.start(tcp_manager) 166 | 167 | actor.run() 168 | -------------------------------------------------------------------------------- /example/pingpong.lua: -------------------------------------------------------------------------------- 1 | local actor = require "luactor" 2 | 3 | local reactor_name = os.getenv("LUACTOR_REACTOR") or 'luaevent' 4 | print('The reactor you use is *'..reactor_name..'*.') 5 | 6 | local ping = function () 7 | print("ping start") 8 | for _ = 1,100 do 9 | actor.wait({ 10 | bar = function (msg, sender) 11 | print(string.format('msg from:%s msg:%s', sender, msg)) 12 | actor.send("pong", "foo", "hello") 13 | end, 14 | }) 15 | end 16 | end 17 | 18 | local pong = function () 19 | print("pong start") 20 | for _ = 1,100 do 21 | actor.wait({ 22 | foo = function (msg, sender) 23 | print(string.format('msg from:%s msg:%s', sender, msg)) 24 | actor.send("ping", "bar", "world") 25 | end, 26 | }) 27 | end 28 | end 29 | 30 | pinger = actor.create('ping', ping) 31 | ponger = actor.create('pong', pong) 32 | 33 | actor.start(pinger) 34 | actor.start(ponger) 35 | 36 | actor.send('ping', 'bar', 'world') 37 | 38 | actor.run() 39 | -------------------------------------------------------------------------------- /example/timeout.lua: -------------------------------------------------------------------------------- 1 | local actor = require "luactor" 2 | 3 | -- Setup reactor--------------------------------------------------------------- 4 | local reactor_name = os.getenv("LUACTOR_REACTOR") or 'luaevent' 5 | print('The reactor you use is *'..reactor_name..'*.') 6 | 7 | local timeout = function () 8 | print ('Timer Actor start...') 9 | print ('Register 1s timeout.') 10 | -- register timeout event 11 | actor.register_event( 12 | 'timeout', -- event type 13 | 'to1', -- event name 14 | 1 -- event parameters 15 | ) 16 | 17 | print ('Wait 1s') 18 | actor.wait({ 19 | to1 = function (msg, from) 20 | print( 21 | string.format( 22 | 'Got message from:%s', 23 | from 24 | ) 25 | ) 26 | end, 27 | }) 28 | print ('Time Actor end...') 29 | end 30 | 31 | timer = actor.create('timeout', timeout) 32 | 33 | actor.start(timer) 34 | 35 | actor.run() 36 | -------------------------------------------------------------------------------- /luactor.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Luactor - A pure Lua "Actor Model" framework 3 | -- 4 | 5 | --============================================================================ 6 | -- load configuration module 7 | local __config = require "luactor.config" 8 | --============================================================================ 9 | -- An actor need a reactor for event trigger. 10 | 11 | -- get the choose of reactor from environment variable. 12 | local reactor_env = os.getenv("LUACTOR_REACTOR") 13 | 14 | -- get reactor choice from config module if no environment variable set. 15 | if reactor_env == nil then 16 | reactor_env = __config.REACTOR 17 | end 18 | 19 | -- load rector library 20 | local reactor 21 | if reactor_env == 'uloop' then 22 | reactor = require "luactor.reactor.uloop" 23 | else 24 | -- for now, default reactor driver is luaevent 25 | reactor = require "luactor.reactor.luaevent" 26 | end 27 | --============================================================================ 28 | -- declare internal objects 29 | local __reactor -- reactor object 30 | local __mqueue -- message queue 31 | local __actors -- actor object pool 32 | local __actors_num -- number of actors 33 | local __lut_thread_actor -- thread to actor look-up table 34 | local __actors_events -- registered events for each actor 35 | 36 | --============================================================================ 37 | -- helper methods for luactor 38 | 39 | local pack = table.pack or function(...) return {n = select("#", ...), ...} end 40 | 41 | ------------------------------------------------------------------------------ 42 | -- open a coroutine safely (for lua 5.1) 43 | -- modified from coxpcall project 44 | -- 45 | -- Coroutine safe xpcall and pcall versions modified for luactor 46 | -- 47 | -- Encapsulates the protected calls with a coroutine based loop, so errors can 48 | -- be dealed without the usual Lua 5.x pcall/xpcall issues with coroutines 49 | -- yielding inside the call to pcall or xpcall. 50 | -- 51 | -- Authors: Roberto Ierusalimschy and Andre Carregal 52 | -- Contributors: Thomas Harning Jr., Ignacio Burgueño, Fabio Mascarenhas 53 | -- 54 | -- Copyright 2005 - Kepler Project (www.keplerproject.org) 55 | ------------------------------------------------------------------------------- 56 | 57 | local handle_return_value = function (err, co, status, ...) 58 | if not status then 59 | return false, err(debug.traceback(co, (...)), ...) 60 | end 61 | return true, ... 62 | end 63 | 64 | local perform_resume = function (err, co, ...) 65 | return handle_return_value(err, co, coroutine.resume(co, ...)) 66 | end 67 | 68 | local safe_coroutine_create = function(f) 69 | local res, thread = pcall(coroutine.create, f) 70 | if not res then 71 | local newf = function(...) return f(...) end 72 | thread = coroutine.create(newf) 73 | end 74 | return thread 75 | end 76 | 77 | -------------------------------------------------------------------------------- 78 | -- Simple Queue 79 | 80 | local queue = {} 81 | 82 | queue.new = function() 83 | return {first = 0, last = -1} 84 | end 85 | 86 | queue.push = function (q, value) 87 | local first = q.first - 1 88 | q.first = first 89 | q[first] = value 90 | end 91 | 92 | queue.pop = function (q) 93 | local last = q.last 94 | if q.first > last then error("queue is empty") end 95 | local value = q[last] 96 | q[last] = nil -- to allow garbage collection 97 | q.last = last - 1 98 | return value 99 | end 100 | 101 | queue.empty = function (q) 102 | return q.first > q.last and true or false 103 | end 104 | 105 | ------------------------------------------------------------------------------- 106 | -- get running actor's object 107 | local get_myself = function () 108 | local myself_thread = coroutine.running() 109 | local me = __lut_thread_actor[myself_thread] 110 | -- TODO: what if I am main thread 111 | return me 112 | end 113 | 114 | ------------------------------------------------------------------------------- 115 | -- resume an actor and handle its error. 116 | local resume_actor = function (actor, ...) 117 | local actor = actor 118 | if type(actor) == 'string' then 119 | actor = __actors[actor] 120 | end 121 | 122 | local me = get_myself() 123 | local status, err = perform_resume(pack, actor.thread, ...) 124 | -- send the failed actor to its creator. if it creator is 125 | -- dead, then just destory it. 126 | if status == false and actor.creator ~= nil then 127 | queue.push(__mqueue, { 128 | '_', -- sender 129 | actor.creator, -- receiver 130 | "actor_error", -- command 131 | { 132 | actor = actor, 133 | error = err, 134 | } -- error message 135 | }) 136 | end 137 | 138 | -- if an actor is dead or failed, destory the relevant info. 139 | if coroutine.status(actor.thread) == 'dead' 140 | or status == false 141 | then 142 | __actors[actor.name] = nil 143 | __actors_num = __actors_num - 1 144 | __lut_thread_actor[actor.thread] = nil 145 | 146 | -- unregister all events that created by this actor 147 | for _, ev_obj in pairs(__actors_events[actor.name]) do 148 | __reactor.unregister_event(ev_obj) 149 | end 150 | __actors_events[actor.name] = nil 151 | end 152 | end 153 | 154 | ------------------------------------------------------------------------------ 155 | -- event register handlers 156 | local event_handlers = { 157 | timeout = function(sender, receiver, ev_name, timeout_interval) 158 | return __reactor.register_timeout_cb( 159 | function (events) 160 | -- push event message to __mqueue 161 | queue.push(__mqueue, {sender, receiver, ev_name, 162 | { 163 | timeout_interval = timeout_interval, 164 | } 165 | }) 166 | resume_actor('_') 167 | end, timeout_interval 168 | ) 169 | end, 170 | fd = function(sender, receiver, ev_name, fd, event) 171 | return __reactor.register_fd_event( 172 | function (events) 173 | -- push event message to __mqueue 174 | queue.push(__mqueue, {sender, receiver, ev_name, 175 | { 176 | event = event, 177 | fd = fd, 178 | } 179 | }) 180 | resume_actor('_') 181 | end, 182 | fd, event 183 | ) 184 | end, 185 | } 186 | 187 | ------------------------------------------------------------------------------ 188 | -- schedular coroutine 189 | -- 190 | -- process the message queue until it's empty, then yield out. 191 | local process_mqueue = function () 192 | local finish = false 193 | while not finish do 194 | -- process the message in the queue one by one until empty. 195 | while not queue.empty(__mqueue) do 196 | local sender, receiver, command, message = unpack(queue.pop(__mqueue)) 197 | if __actors[receiver] == nil then 198 | -- drop this message 199 | -- TODO: what we can do before the message is dropped. 200 | else 201 | resume_actor(receiver, sender, command, message) 202 | 203 | -- if there is no running actor, everything should be done. 204 | if __actors_num <= 0 then 205 | __reactor.cancel() 206 | finish = true 207 | break 208 | end 209 | end 210 | end 211 | -- yield out to mainthread to wait next event. 212 | coroutine.yield() 213 | end 214 | end 215 | 216 | --============================================================================ 217 | -- Luactor methods 218 | -- 219 | -- like *coroutine*, all *luactor* method are contained within a table. 220 | 221 | local actor = {} 222 | 223 | ------------------------------------------------------------------------------ 224 | -- initialize internal objects 225 | __reactor = reactor -- reactor object 226 | __mqueue = queue.new() -- message queue 227 | __actors = {} -- actor object pool 228 | __actors_num = 0 -- number of actors 229 | __lut_thread_actor = {} -- thread to actor look-up table 230 | __actors_events = {} -- registered events for each actor 231 | 232 | ------------------------------------------------------------------------------ 233 | -- create an actor 234 | actor.create = function (name, f, ...) 235 | if __actors[name] ~= nil then 236 | error("the actor name has been registered") 237 | end 238 | 239 | local new_actor = {} 240 | new_actor.name = name 241 | __actors_num = __actors_num + 1 242 | 243 | local thread = safe_coroutine_create(f) 244 | 245 | new_actor.thread = thread 246 | 247 | local me = get_myself() 248 | new_actor.creator = me and me.name 249 | 250 | -- save the actor to the global table 251 | __actors[name] = new_actor 252 | __actors_events[name] = {} 253 | __lut_thread_actor[thread] = new_actor 254 | 255 | return new_actor 256 | end 257 | 258 | ------------------------------------------------------------------------------ 259 | -- send a message to another actor 260 | -- 261 | -- XXX: should we yield out when receive a message? 262 | actor.send = function (receiver, command, message) 263 | local me = get_myself() 264 | 265 | -- didn't check receiver's name because it might be create later. 266 | 267 | -- push message to global message queue 268 | queue.push(__mqueue, { 269 | me and me.name or "_", -- sender 270 | receiver, -- receiver 271 | command, -- command 272 | message, -- message 273 | }) 274 | 275 | end 276 | 277 | ------------------------------------------------------------------------------ 278 | -- wait a message 279 | actor.wait = function (handlers) 280 | -- handlers is the set of the message you want listen 281 | local sender, command, message = coroutine.yield() 282 | if command ~= nil 283 | and handlers[command] ~= nil 284 | -- XXX:how to make sure handlers[command] is function like? 285 | then 286 | handlers[command](message, sender) 287 | else 288 | -- XXX: should we raise an error here? or, we can return a 289 | -- command `__unknown` or `__index` as a *meta method*. 290 | error("unknown message command type") 291 | end 292 | end 293 | 294 | ------------------------------------------------------------------------------ 295 | -- register an event 296 | actor.register_event = function (ev_type, ev_name, ...) 297 | local me = get_myself() 298 | local event_handler = event_handlers[ev_type] 299 | 300 | if __actors_events[me.name][ev_name] ~= nil then 301 | error('event name "'..ev_name..'" has been registered!') 302 | elseif event_handler ~= nil then 303 | -- XXX: any other possibilities for sender and receiver? 304 | __actors_events[me.name][ev_name] = event_handler('_', me.name, ev_name, ...) 305 | else 306 | error('unknonw event type: '..ev_type) 307 | end 308 | end 309 | 310 | ------------------------------------------------------------------------------ 311 | -- unregister an event 312 | actor.unregister_event = function (ev_name) 313 | local me = get_myself() 314 | if __actors_events[me.name][ev_name] == nil then 315 | error("cannot find event "..ev_name..".") 316 | end 317 | __reactor.unregister_event(__actors_events[me.name][ev_name]) 318 | __actors_events[me.name][ev_name] = nil 319 | end 320 | 321 | ------------------------------------------------------------------------------ 322 | -- start an actor 323 | actor.start = function (actor, ...) 324 | resume_actor(actor, ...) 325 | end 326 | 327 | ------------------------------------------------------------------------------ 328 | -- run schedular and event loop 329 | actor.run = function () 330 | local thread = safe_coroutine_create(process_mqueue) 331 | 332 | local schedular_actor = {} 333 | schedular_actor.name = '_' 334 | schedular_actor.thread = thread 335 | 336 | __lut_thread_actor[thread] = schedular_actor 337 | 338 | -- TODO: the schedular coroutine should have its creator and the 339 | -- error should be processed. 340 | -- schedular_actor.creater = ? 341 | 342 | __actors['_'] = schedular_actor 343 | resume_actor(schedular_actor) 344 | 345 | -- run until there are no __actors 346 | while __actors_num > 0 do 347 | __reactor.run() 348 | end 349 | 350 | -- TODO: clean up everything 351 | end 352 | 353 | return actor 354 | -------------------------------------------------------------------------------- /luactor/config.lua: -------------------------------------------------------------------------------- 1 | -- You can write your default configuration here. 2 | return { 3 | REACTOR="luaevent", -- or uloop 4 | } 5 | -------------------------------------------------------------------------------- /luactor/reactor/luaevent.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- the luaevent reactor driver 3 | -- 4 | 5 | -- we just use the luaevent.core but not its high-level i/f. 6 | local luaevent = require "luaevent" 7 | local core = luaevent.core 8 | 9 | local luaevent_reactor = {} 10 | 11 | -- event core 12 | local __ev_base = core.new() 13 | 14 | -- registered events pool 15 | local __events = {} 16 | 17 | local __register_event = function (event_cb, fd, event, timeout) 18 | local new_ev_obj 19 | 20 | new_ev_obj = __ev_base:addevent(fd, event, 21 | function () 22 | event_cb() 23 | 24 | -- TODO: here just trigger once, try support trigger persist. 25 | return timeout and core.LEAVE or nil 26 | end, 27 | timeout) 28 | 29 | __events[new_ev_obj] = true 30 | return new_ev_obj 31 | end 32 | 33 | luaevent_reactor.register_fd_event = function (fd_event_cb, fd, ev_type) 34 | local events = 0 35 | -- transform event type 36 | -- TODO: support regiser both *read* and *write*. 37 | if ev_type == 'read' then 38 | events = events + core.EV_READ 39 | elseif ev_type == 'write' then 40 | events = events + core.EV_WRITE 41 | else 42 | error("unsupport event type or nothing to do") 43 | end 44 | 45 | return __register_event(fd_event_cb, fd, events) 46 | end 47 | 48 | luaevent_reactor.register_timeout_cb = function (timeout_cb, timeout_interval) 49 | return __register_event(timeout_cb, nil, core.EV_TIMEOUT, timeout_interval) 50 | end 51 | 52 | luaevent_reactor.unregister_event = function (ev_obj) 53 | if __events[ev_obj] ~= true then 54 | error("try to unregister unknown event") 55 | end 56 | 57 | __events[ev_obj] = nil 58 | ev_obj:close() 59 | end 60 | 61 | luaevent_reactor.run = function () 62 | __ev_base:loop() 63 | end 64 | 65 | luaevent_reactor.cancel = function () 66 | -- TODO: impl this method 67 | end 68 | 69 | return luaevent_reactor 70 | -------------------------------------------------------------------------------- /luactor/reactor/reactor_template.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Reactor Interface 3 | -- 4 | -- This is an fake model of a reactor or dispatcher, based on reactor 5 | -- pattern. There are many event-driven based libraries, 6 | -- eg. libevent(luaevent), libev(lua-ev), uloop(OpenWrt) and so on. 7 | -- 8 | -- This interface provide an template of the reactor/dispatcher, 9 | -- so it is easy to port to any other system. 10 | -- 11 | -- A dispather can hold multiple transports. 12 | -- 13 | -- Any reactor MUST follow this interface. 14 | -- 15 | 16 | local your_reactor = {} 17 | 18 | -- 19 | -- method to register a fd event 20 | -- 21 | -- return the event object which is used to unregister 22 | -- 23 | your_reactor.register_fd_event = function (fd_event_cb, fd, event) 24 | error('Method not implemented.') 25 | end 26 | 27 | -- 28 | -- method to register a timeout event 29 | -- 30 | -- return the event object which is used to unregister 31 | -- 32 | your_reactor.register_timeout_cb = function (timeout_cb, timeout_interval) 33 | error('Method not implemented.') 34 | end 35 | 36 | -- 37 | -- method to unregister a timeout or fd event 38 | -- 39 | your_reactor.unregister_event = function (ev_obj) 40 | error('Method not implemented.') 41 | end 42 | 43 | -- 44 | -- run the reactor to start listen all events 45 | -- 46 | your_reactor.run = function () 47 | error('Method not implemented.') 48 | end 49 | 50 | -- 51 | -- cancel the reactor 52 | -- 53 | your_reactor.cancel = function () 54 | error('Method not implemented.') 55 | end 56 | 57 | return your_reactor 58 | -------------------------------------------------------------------------------- /luactor/reactor/uloop.lua: -------------------------------------------------------------------------------- 1 | local uloop = require("uloop") 2 | 3 | -- XXX: for uloop, it is global, so only one uloop_reactor instance 4 | -- can be use. 5 | local uloop_reactor = {} 6 | 7 | local __events = {} 8 | 9 | uloop.init() 10 | 11 | uloop_reactor.register_fd_event = function (fd_event_cb, fd, event) 12 | local new_ev_obj 13 | local events = 0 14 | 15 | -- TODO: support regiser both *read* and *write*. 16 | if event == 'read' then 17 | events = events + uloop.ULOOP_READ 18 | elseif event == 'write' then 19 | events = events + uloop.ULOOP_WRITE 20 | else 21 | error("unsupport event or nothing to do") 22 | end 23 | 24 | new_ev_obj = uloop.fd_add(fd, fd_event_cb, events) 25 | __events[new_ev_obj] = true 26 | 27 | return new_ev_obj 28 | end 29 | 30 | uloop_reactor.register_timeout_cb = function (timeout_cb, timeout_interval) 31 | local new_ev_obj 32 | local ti 33 | 34 | -- the time unit of uloop is ms, so convert it to second. 35 | ti = math.floor(timeout_interval * 1000) 36 | 37 | new_ev_obj = uloop.timer(timeout_cb, ti) 38 | __events[new_ev_obj] = true 39 | 40 | return new_ev_obj 41 | end 42 | 43 | uloop_reactor.unregister_event = function (ev_obj) 44 | if __events[ev_obj] ~= true then 45 | error("try to unregister unknown event") 46 | end 47 | 48 | __events[ev_obj] = nil 49 | 50 | -- for timer event, use cancel to unregister it 51 | -- for fd event, use delete to unregister it 52 | if ev_obj.cancel ~= nil then 53 | ev_obj:cancel() 54 | else 55 | ev_obj:delete() 56 | end 57 | end 58 | 59 | uloop_reactor.run = function () 60 | uloop.run() 61 | end 62 | 63 | uloop_reactor.cancel = function () 64 | uloop.cancel() 65 | end 66 | 67 | return uloop_reactor 68 | -------------------------------------------------------------------------------- /tests/report_coverage.lua: -------------------------------------------------------------------------------- 1 | local json = require ("json") 2 | local runner = require("luacov.runner") 3 | runner.load_config() 4 | 5 | ------------------------ 6 | -- Report module, will transform statistics file into a report. 7 | -- @class module 8 | -- @name luacov.reporter 9 | 10 | --- Utility function to make patterns more readable 11 | local function fixup(pat) 12 | return pat:gsub(" ", " +") -- ' ' represents "at least one space" 13 | :gsub("=", " *= *") -- '=' may be surrounded by spaces 14 | :gsub("%(", " *%%( *") -- '(' may be surrounded by spaces 15 | :gsub("%)", " *%%) *") -- ')' may be surrounded by spaces 16 | :gsub("", " *[%%w_]+ *") -- identifier 17 | :gsub("", " *[%%w._]+ *") -- identifier 18 | :gsub("", "%%[(=*)%%[[^]]* *") 19 | :gsub("", "[%%w_, ]+") -- comma-separated identifiers 20 | :gsub("", "[%%w_, \"'%%.]*") -- comma-separated arguments 21 | :gsub("", "%%[? *[\"'%%w_]+ *%%]?") -- field, possibly like ["this"] 22 | :gsub(" %* ", " ") -- collapse consecutive spacing rules 23 | :gsub(" %+ %*", " +") -- collapse consecutive spacing rules 24 | end 25 | 26 | local long_string_1 = "^() *" .. fixup"=$" 27 | local long_string_2 = "^() *" .. fixup"local =$" 28 | 29 | local function check_long_string(line, in_long_string, ls_equals, linecount) 30 | local long_string 31 | if not linecount then 32 | if line:match("%[=*%[") then 33 | long_string, ls_equals = line:match(long_string_1) 34 | if not long_string then 35 | long_string, ls_equals = line:match(long_string_2) 36 | end 37 | end 38 | end 39 | ls_equals = ls_equals or "" 40 | if long_string then 41 | in_long_string = true 42 | elseif in_long_string and line:match("%]"..ls_equals.."%]") then 43 | in_long_string = false 44 | end 45 | return in_long_string, ls_equals or "" 46 | end 47 | 48 | --- Lines that are always excluded from accounting 49 | local exclusions = { 50 | { false, "^#!" }, -- Unix hash-bang magic line 51 | { true, "" }, -- Empty line 52 | { true, fixup "end,?" }, -- Single "end" 53 | { true, fixup "else" }, -- Single "else" 54 | { true, fixup "repeat" }, -- Single "repeat" 55 | { true, fixup "do" }, -- Single "do" 56 | { true, fixup "while true do" }, -- "while true do" generates no code 57 | { true, fixup "if true then" }, -- "if true then" generates no code 58 | { true, fixup "local " }, -- "local var1, ..., varN" 59 | { true, fixup "local =" }, -- "local var1, ..., varN =" 60 | { true, fixup "local function()" }, -- "local function(arg1, ..., argN)" 61 | { true, fixup "local function ()" }, -- "local function f (arg1, ..., argN)" 62 | } 63 | 64 | --- Lines that are only excluded from accounting when they have 0 hits 65 | local hit0_exclusions = { 66 | { true, "[%w_,='\" ]+," }, -- "var1 var2," multi columns table stuff 67 | { true, fixup "=.+," }, -- "[123] = 23," "['foo'] = "asd"," 68 | { true, fixup "*function()" }, -- "1,2,function(...)" 69 | { true, fixup "function()" }, -- "local a = function(arg1, ..., argN)" 70 | { true, fixup "local =function()" }, -- "local a = function(arg1, ..., argN)" 71 | { true, fixup "=function()" }, -- "a = function(arg1, ..., argN)" 72 | { true, fixup "break" }, -- "break" generates no trace in Lua 5.2 73 | { true, "{" }, -- "{" opening table 74 | { true, "}" }, -- "{" closing table 75 | { true, fixup "})" }, -- function closer 76 | { true, fixup ")" }, -- function closer 77 | } 78 | 79 | ------------------------ 80 | -- Starts the report generator 81 | -- To load a config, use luacov.runner to load 82 | -- settings and then start the report. 83 | -- @example# local runner = require("luacov.runner") 84 | -- local reporter = require("luacov.reporter") 85 | -- runner.load_config() 86 | -- table.insert(luacov.configuration.include, "thisfile") 87 | -- reporter.report() 88 | function report() 89 | local luacov = require("luacov.runner") 90 | local stats = require("luacov.stats") 91 | 92 | local configuration = luacov.load_config() 93 | stats.statsfile = configuration.statsfile 94 | 95 | local data, most_hits = stats.load() 96 | 97 | if not data then 98 | print("Could not load stats file "..configuration.statsfile..".") 99 | print("Run your Lua program with -lluacov and then rerun luacov.") 100 | os.exit(1) 101 | end 102 | 103 | local report_json_file = io.open(configuration.reportfile, "w") 104 | 105 | local names = {} 106 | for filename, _ in pairs(data) do 107 | local include = false 108 | -- normalize paths in patterns 109 | local path = filename:gsub("\\", "/"):gsub("%.lua$", "") 110 | if not configuration.include[1] then 111 | include = true 112 | else 113 | include = false 114 | for _, p in ipairs(configuration.include) do 115 | if path:match(p) then 116 | include = true 117 | break 118 | end 119 | end 120 | end 121 | if include and configuration.exclude[1] then 122 | for _, p in ipairs(configuration.exclude) do 123 | if path:match(p) then 124 | include = false 125 | break 126 | end 127 | end 128 | end 129 | if include then 130 | table.insert(names, filename) 131 | end 132 | end 133 | 134 | table.sort(names) 135 | 136 | local empty_format = json.decode('null') 137 | local zero_format = 0 138 | 139 | local function excluded(exclusions,line) 140 | for _, e in ipairs(exclusions) do 141 | if e[1] then 142 | if line:match("^ *"..e[2].." *$") or line:match("^ *"..e[2].." *%-%-") then return true end 143 | else 144 | if line:match(e[2]) then return true end 145 | end 146 | end 147 | return false 148 | end 149 | 150 | report_json = {} 151 | report_json.service_name = 'travis-ci' 152 | report_json.service_job_id = os.getenv("TRAVIS_JOB_ID") 153 | local source_files = json.util.InitArray({}) 154 | 155 | for _, filename in ipairs(names) do 156 | local filedata = data[filename] 157 | local file = io.open(filename, "r") 158 | if file then 159 | local source_file = {} 160 | local coverage = json.util.InitArray({}) 161 | source_file.name = filename 162 | 163 | local line_nr = 1 164 | local file_hits, file_miss = 0, 0 165 | local block_comment, equals = false, "" 166 | local in_long_string, ls_equals = false, "" 167 | 168 | while true do 169 | local line = file:read("*l") 170 | if not line then break end 171 | local true_line = line 172 | 173 | local new_block_comment = false 174 | if not block_comment then 175 | line = line:gsub("%s+", " ") 176 | local l, equals = line:match("^(.*)%-%-%[(=*)%[") 177 | if l then 178 | line = l 179 | new_block_comment = true 180 | end 181 | in_long_string, ls_equals = check_long_string(line, in_long_string, ls_equals, filedata[line_nr]) 182 | else 183 | local l = line:match("%]"..equals.."%](.*)$") 184 | if l then 185 | line = l 186 | block_comment = false 187 | end 188 | end 189 | 190 | local hits = filedata[line_nr] or 0 191 | if block_comment or in_long_string or excluded(exclusions,line) or (hits == 0 and excluded(hit0_exclusions,line)) then 192 | table.insert(coverage, empty_format) 193 | else 194 | if hits == 0 then 195 | file_miss = file_miss + 1 196 | table.insert(coverage, zero_format) 197 | else 198 | file_hits = file_hits + 1 199 | table.insert(coverage, hits) 200 | end 201 | end 202 | if new_block_comment then block_comment = true end 203 | line_nr = line_nr + 1 204 | end 205 | file:close() 206 | 207 | local file = io.open(filename, "r") 208 | source_file.source = file:read("*all") 209 | file:close() 210 | 211 | source_file.coverage = coverage 212 | table.insert(source_files, source_file) 213 | end 214 | end 215 | report_json.source_files = source_files 216 | 217 | report_json_file:write(json.encode(report_json)) 218 | report_json_file:close() 219 | 220 | if configuration.deletestats then 221 | os.remove(configuration.statsfile) 222 | end 223 | end 224 | 225 | report() 226 | 227 | -------------------------------------------------------------------------------- /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | echo "=======================================" 4 | echo "PingPong example test" 5 | $LUA -v -lluacov example/pingpong.lua $REACTOR 6 | if (($?)); then return 1; fi 7 | 8 | echo "=======================================" 9 | echo "Timeout example test" 10 | $LUA -v -lluacov example/timeout.lua $REACTOR 11 | if (($?)); then return 1; fi 12 | 13 | echo "=======================================" 14 | echo "Echo server example test" 15 | python tests/test_echo-server.py "$LUA -v -lluacov example/echo-server.lua $REACTOR" 16 | if (($?)); then 17 | cat tests/test_echo-server.log | col 18 | return 1 19 | fi 20 | cat tests/test_echo-server.log | col 21 | 22 | -------------------------------------------------------------------------------- /tests/setup.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | sudo apt-get install -y $LUA $lib$LUA-0-dev luarocks python-pexpect bsdmainutils telnet 4 | 5 | sudo luarocks install luacov 6 | sudo luarocks install luajson 7 | sudo luarocks install luasocket 8 | 9 | if [ $LUACTOR_REACTOR = "luaevent" ]; then 10 | sudo apt-get install libevent-dev -y && 11 | rm -rf luaevent && 12 | git clone https://github.com/harningt/luaevent.git && 13 | sudo make -C luaevent install ; 14 | fi 15 | 16 | if [ $LUACTOR_REACTOR = "uloop" ]; then 17 | rm -rf libubox && 18 | git clone https://github.com/xfguo/libubox.git && 19 | ( 20 | cd libubox && 21 | cmake . && 22 | make 23 | ) && 24 | ln -sf libubox/libubox.so . && 25 | ln -sf libubox/lua/uloop.so ; 26 | fi 27 | -------------------------------------------------------------------------------- /tests/test_echo-server.py: -------------------------------------------------------------------------------- 1 | import pexpect 2 | import sys 3 | import time 4 | 5 | logfile = open("tests/test_echo-server.log", 'w') 6 | 7 | try: 8 | print "TEST START ..." 9 | prog_tested = pexpect.spawn(sys.argv[1], timeout = 100, logfile = logfile) 10 | time.sleep(1) 11 | 12 | prog_tested.expect("TcpManager start...") 13 | 14 | print ">>> Test raise an error." 15 | c_err = pexpect.spawn('telnet 127.0.0.1 48888', timeout = 300, logfile = logfile) 16 | time.sleep(1) 17 | c_err.sendline("hello") 18 | c_err.expect("hello") 19 | c_err.sendline("raise") 20 | c_err.expect("raise") 21 | prog_tested.expect("trace") 22 | prog_tested.expect("error") 23 | 24 | c = dict() 25 | for i in range(6): 26 | print ">>> Open a new telnet session #%d" % i 27 | c[i] = pexpect.spawn('telnet 127.0.0.1 48888', timeout = 100, logfile = logfile) 28 | time.sleep(1) 29 | prog_tested.expect("EchoActor.*start") 30 | 31 | print ">>> Send some words and wait for echo with #%d" % i 32 | for j in range(i + 1): 33 | c[i].sendline(str(i) * (j + 1)) 34 | c[i].expect(str(i) * (j + 1)) 35 | 36 | if i % 2 == 0: 37 | print ">>> Close connection by send Ctrl-D for conn #%d" % i 38 | c[i].sendcontrol("]") 39 | c[i].expect("telnet>") 40 | c[i].sendcontrol("D") 41 | c[i].expect(".*") 42 | time.sleep(1) 43 | prog_tested.expect("EchoActor.*end") 44 | 45 | prog_tested.expect(".*") 46 | 47 | print ">>> Send 'exit' to close all connections then exit" 48 | cexit = pexpect.spawn('telnet 127.0.0.1 48888', timeout = 100, logfile = logfile) 49 | time.sleep(1) 50 | cexit.sendline("exit") 51 | cexit.expect("exit") 52 | prog_tested.expect("TcpManager end...") 53 | 54 | except Exception, e: 55 | print "v" * 80 56 | print e 57 | print "^" * 80 58 | print "TEST FAILED!" 59 | sys.exit(-1) 60 | --------------------------------------------------------------------------------