├── .gitignore ├── ext ├── extconf.rb └── Makefile ├── .document ├── ebin └── ernie_server_app.app ├── examples ├── ext.erl ├── example.config ├── nat.erl ├── example.cfg └── ext.rb ├── test ├── sample │ ├── sample.cfg │ ├── ext.rb │ └── intTest.erl ├── load.rb ├── helper.rb ├── test_ernie.rb └── test_ernie_server.rb ├── elib ├── logger_sup.erl ├── asset_pool_sup.erl ├── ernie_server_app.erl ├── ernie_access_logger_sup.erl ├── ernie_server_sup.erl ├── ernie_config.erl ├── ernie_native.erl ├── ernie.hrl ├── bert.erl ├── port_wrapper.erl ├── ernie_admin.erl ├── logger.erl ├── asset_pool.erl ├── ernie_access_logger.erl └── ernie_server.erl ├── LICENSE ├── History.txt ├── ernie.gemspec ├── contrib └── ebench.erl ├── bin └── ernie ├── Rakefile ├── lib └── ernie.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | pkg 3 | -------------------------------------------------------------------------------- /ext/extconf.rb: -------------------------------------------------------------------------------- 1 | # does nothing, Makefile is handwritten -------------------------------------------------------------------------------- /ext/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | erlc -o ../ebin ../elib/*.erl -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.md 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /ebin/ernie_server_app.app: -------------------------------------------------------------------------------- 1 | {application, ernie_server_app, 2 | [{mod, {ernie_server_app, []}}]}. -------------------------------------------------------------------------------- /examples/ext.erl: -------------------------------------------------------------------------------- 1 | -module(ext). 2 | -export([shadow_pred/1, shadow/1]). 3 | 4 | shadow_pred(X) -> 5 | X > 10. 6 | 7 | shadow(_X) -> 8 | <<"erlang">>. -------------------------------------------------------------------------------- /test/sample/sample.cfg: -------------------------------------------------------------------------------- 1 | [{module, ext}, 2 | {type, external}, 3 | {command, "ruby test/sample/ext.rb"}, 4 | {count, 1}]. 5 | [{module, intTest}, 6 | {type, native}, 7 | {codepaths, ["test/sample"]} 8 | ]. -------------------------------------------------------------------------------- /examples/example.config: -------------------------------------------------------------------------------- 1 | [{sasl, 2 | [{sasl_error_logger, {file, "/tmp/sasl/sasl.log"}}, 3 | {error_logger_mf_dir, "/tmp/sasl"}, 4 | {error_logger_mf_maxbytes, 10000000}, 5 | {error_logger_mf_maxfiles, 5}]}]. -------------------------------------------------------------------------------- /examples/nat.erl: -------------------------------------------------------------------------------- 1 | -module(nat). 2 | -export([add/2, fib/1, die/1]). 3 | 4 | add(A, B) -> 5 | A + B. 6 | 7 | fib(0) -> 1; 8 | fib(1) -> 1; 9 | fib(N) when N > 1 -> fib(N - 1) + fib(N - 2). 10 | 11 | die(X) -> 12 | X = 10. -------------------------------------------------------------------------------- /examples/example.cfg: -------------------------------------------------------------------------------- 1 | [{module, ext}, 2 | {type, native}, 3 | {codepaths, ["examples"]}]. 4 | 5 | [{module, ext}, 6 | {type, external}, 7 | {command, "ruby examples/ext.rb"}, 8 | {count, 2}]. 9 | 10 | [{module, nat}, 11 | {type, native}, 12 | {codepaths, ["examples"]}]. -------------------------------------------------------------------------------- /test/load.rb: -------------------------------------------------------------------------------- 1 | require 'bertrpc' 2 | 3 | $stdout.sync = true 4 | 5 | threads = [] 6 | svc = BERTRPC::Service.new('localhost', 8000) 7 | 8 | 8.times do 9 | threads << Thread.new do 10 | i = 0 11 | 10.times { i += svc.call.calc.add(1, 2); print '.'; $stdout.flush } 12 | print "(#{i})" 13 | end 14 | end 15 | 16 | threads.each { |t| t.join } 17 | 18 | puts -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | require 'shoulda' 4 | 5 | ERNIE_ROOT = File.expand_path(File.join(File.dirname(__FILE__), *%w[..])) 6 | 7 | $:.unshift(File.join(ERNIE_ROOT, 'lib')) 8 | 9 | require 'ernie' 10 | Ernie.auto_start = false 11 | 12 | begin 13 | require 'bertrpc' 14 | rescue LoadError 15 | puts "You need bertrpc gem installed to run tests." 16 | exit!(1) 17 | end 18 | -------------------------------------------------------------------------------- /elib/logger_sup.erl: -------------------------------------------------------------------------------- 1 | -module(logger_sup). 2 | -behaviour(supervisor). 3 | -export([start_link/0, init/1]). 4 | 5 | start_link() -> 6 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 7 | 8 | init([]) -> 9 | {ok, LogLevel} = application:get_env(ernie_server_app, log_level), 10 | io:format("Using log level ~p~n", [LogLevel]), 11 | {ok, {{one_for_one, 1, 60}, 12 | [{logger, {logger, start_link, [[LogLevel]]}, 13 | permanent, brutal_kill, worker, [logger]}]}}. -------------------------------------------------------------------------------- /elib/asset_pool_sup.erl: -------------------------------------------------------------------------------- 1 | -module(asset_pool_sup). 2 | -behaviour(supervisor). 3 | -export([start_link/2, init/1]). 4 | 5 | start_link(Handler, Number) -> 6 | supervisor:start_link(?MODULE, [Handler, Number]). 7 | 8 | init([Handler, Number]) -> 9 | io:format("Using handler ~p~n", [Handler]), 10 | io:format("Using ~p handler instances~n", [Number]), 11 | {ok, {{one_for_one, 1, 60}, 12 | [{asset_pool, {asset_pool, start_link, [Handler, Number]}, 13 | permanent, brutal_kill, worker, [asset_pool]}]}}. -------------------------------------------------------------------------------- /elib/ernie_server_app.erl: -------------------------------------------------------------------------------- 1 | -module(ernie_server_app). 2 | -behaviour(application). 3 | 4 | -export([boot/0, start/2, stop/1]). 5 | 6 | boot() -> 7 | application:start(ernie_server_app). 8 | 9 | start(_Type, _Args) -> 10 | case application:get_env(ernie_server_app, access_log) of 11 | {ok, AccessFile} -> 12 | ernie_access_logger_sup:start_link(AccessFile); 13 | undefined -> 14 | ernie_access_logger_sup:start_link(undefined) 15 | end, 16 | logger_sup:start_link(), 17 | ernie_server_sup:start_link(). 18 | 19 | stop(_State) -> 20 | ok. 21 | -------------------------------------------------------------------------------- /elib/ernie_access_logger_sup.erl: -------------------------------------------------------------------------------- 1 | -module(ernie_access_logger_sup). 2 | -behaviour(supervisor). 3 | -export([start_link/1, init/1]). 4 | 5 | start_link(AccessLog) -> 6 | supervisor:start_link({local, ?MODULE}, ?MODULE, [AccessLog]). 7 | 8 | init([AccessLog]) -> 9 | case AccessLog of 10 | undefined -> io:format("No access log~n", []); 11 | Any -> io:format("Using access log ~p~n", [Any]) 12 | end, 13 | {ok, {{one_for_one, 1, 60}, 14 | [{ernie_access_logger, {ernie_access_logger, start_link, [[AccessLog]]}, 15 | permanent, brutal_kill, worker, [ernie_access_logger]}]}}. -------------------------------------------------------------------------------- /test/sample/ext.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib')) 2 | require 'ernie' 3 | 4 | module Ext 5 | @@state = 0 6 | 7 | def zeronary 8 | :foo 9 | end 10 | 11 | def unary(a) 12 | a 13 | end 14 | 15 | def binary(a, b) 16 | a + b 17 | end 18 | 19 | def ternary(a, b, c) 20 | a + b + c 21 | end 22 | 23 | def set_state(x) 24 | @@state = x 25 | sleep 5 26 | nil 27 | end 28 | 29 | def get_state 30 | @@state 31 | end 32 | 33 | def big(x) 34 | 'a' * x 35 | end 36 | 37 | def cry 38 | raise "abandon hope!" 39 | end 40 | end 41 | 42 | Ernie.expose(:ext, Ext) -------------------------------------------------------------------------------- /examples/ext.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'ernie' 3 | 4 | module Ext 5 | # Add two numbers together 6 | def add(a, b) 7 | a + b 8 | end 9 | 10 | def fib(n) 11 | if n == 0 || n == 1 12 | 1 13 | else 14 | fib(n - 1) + fib(n - 2) 15 | end 16 | end 17 | 18 | def shadow(x) 19 | "ruby" 20 | end 21 | 22 | # Return the given number of bytes 23 | def bytes(n) 24 | 'x' * n 25 | end 26 | 27 | # Sleep for +sec+ and then return :ok 28 | def slow(sec) 29 | sleep(sec) 30 | :ok 31 | end 32 | 33 | # Throw an error 34 | def error 35 | raise "abandon hope!" 36 | end 37 | end 38 | 39 | Ernie.expose(:ext, Ext) -------------------------------------------------------------------------------- /elib/ernie_server_sup.erl: -------------------------------------------------------------------------------- 1 | -module(ernie_server_sup). 2 | -behaviour(supervisor). 3 | -export([start_link/0, init/1]). 4 | 5 | start_link() -> 6 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 7 | 8 | init([]) -> 9 | {ok, Port} = application:get_env(ernie_server_app, port), 10 | io:format("Using port ~p~n", [Port]), 11 | case application:get_env(ernie_server_app, pidfile) of 12 | {ok, Location} -> 13 | Pid = os:getpid(), 14 | ok = file:write_file(Location, list_to_binary(Pid)); 15 | undefined -> ok 16 | end, 17 | {ok, Config} = application:get_env(ernie_server_app, config), 18 | {ok, Configs} = ernie_config:load(Config), 19 | io:format("~p~n", [Configs]), 20 | {ok, {{one_for_one, 1, 60}, 21 | [{ernie_server, {ernie_server, start_link, [[Port, Configs]]}, 22 | permanent, brutal_kill, worker, [ernie_server]}]}}. -------------------------------------------------------------------------------- /elib/ernie_config.erl: -------------------------------------------------------------------------------- 1 | -module(ernie_config). 2 | -export([load/1]). 3 | 4 | load(ConfigFile) -> 5 | {ok, Configs} = file:consult(ConfigFile), 6 | Configs2 = lists:map((fun load_single/1), Configs), 7 | {ok, Configs2}. 8 | 9 | load_single(Config) -> 10 | case proplists:get_value(type, Config) of 11 | native -> 12 | verify(native, Config), 13 | CodePaths = proplists:get_value(codepaths, Config), 14 | lists:map((fun code:add_patha/1), CodePaths), 15 | Mod = proplists:get_value(module, Config), 16 | code:load_file(Mod), 17 | [{id, native} | Config]; 18 | external -> 19 | verify(external, Config), 20 | Handler = proplists:get_value(command, Config), 21 | Number = proplists:get_value(count, Config), 22 | {ok, SupPid} = asset_pool_sup:start_link(Handler, Number), 23 | [{_Id, ChildPid, _Type, _Modules}] = supervisor:which_children(SupPid), 24 | [{id, ChildPid} | Config] 25 | end. 26 | 27 | verify(native, _Config) -> 28 | ok; 29 | verify(external, _Config) -> 30 | ok. -------------------------------------------------------------------------------- /elib/ernie_native.erl: -------------------------------------------------------------------------------- 1 | -module(ernie_native). 2 | -export([process/2]). 3 | -include_lib("ernie.hrl"). 4 | 5 | process(ActionTerm, Request) -> 6 | {_Type, Mod, Fun, Args} = ActionTerm, 7 | Sock = Request#request.sock, 8 | logger:debug("Calling ~p:~p(~p)~n", [Mod, Fun, Args]), 9 | try apply(Mod, Fun, Args) of 10 | Result -> 11 | logger:debug("Result was ~p~n", [Result]), 12 | Data = bert:encode({reply, Result}), 13 | gen_tcp:send(Sock, Data) 14 | catch 15 | error:Error -> 16 | BError = list_to_binary(io_lib:format("~p", [Error])), 17 | Trace = erlang:get_stacktrace(), 18 | BTrace = lists:map(fun(X) -> list_to_binary(io_lib:format("~p", [X])) end, Trace), 19 | Data = term_to_binary({error, [user, 0, <<"RuntimeError">>, BError, BTrace]}), 20 | gen_tcp:send(Sock, Data) 21 | end, 22 | ok = gen_tcp:close(Sock), 23 | ernie_server:fin(), 24 | Log = Request#request.log, 25 | Log2 = Log#log{tdone = erlang:now()}, 26 | Request2 = Request#request{log = Log2}, 27 | ernie_access_logger:acc(Request2). -------------------------------------------------------------------------------- /test/test_ernie.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class ErnieTest < Test::Unit::TestCase 4 | module TestExposingModule 5 | def foo 6 | end 7 | 8 | def bar(a, b, c=nil) 9 | [a, b, c] 10 | end 11 | 12 | protected 13 | def baz 14 | end 15 | 16 | private 17 | def bling 18 | end 19 | end 20 | 21 | context "expose" do 22 | setup { Ernie.expose :expo, TestExposingModule } 23 | teardown { Ernie.mods.clear } 24 | 25 | should "add all public methods from the module" do 26 | assert_not_nil Ernie.mods[:expo].funs[:foo] 27 | assert_not_nil Ernie.mods[:expo].funs[:bar] 28 | end 29 | 30 | should "not expose protected methods" do 31 | assert_nil Ernie.mods[:expo].funs[:baz] 32 | end 33 | 34 | should "not expose private methods" do 35 | assert_nil Ernie.mods[:expo].funs[:bling] 36 | end 37 | 38 | should "dispatch to module methods properly" do 39 | res = Ernie.dispatch(:expo, :bar, ['a', :b, { :fiz => 42 }]) 40 | assert_equal ['a', :b, { :fiz => 42 }], res 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Tom Preston-Werner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/sample/intTest.erl: -------------------------------------------------------------------------------- 1 | -module(intTest). 2 | 3 | -export([zeronary/0, unary/1, binary/2, ternary/3, big/1, set_state/1, get_state/0, connect_nodes/0, sleep/1]). 4 | 5 | connect_nodes() -> 6 | net_adm:ping('ernie0@127.0.0.1'). 7 | 8 | zeronary() -> 9 | foo. 10 | 11 | unary(A) -> 12 | A. 13 | 14 | binary(A,B) -> 15 | A + B. 16 | 17 | ternary(A,B,C) -> 18 | A + B + C. 19 | 20 | big(A) -> 21 | string:copies("a", A). 22 | 23 | sleep(Time) -> 24 | receive after Time -> 25 | ok 26 | end. 27 | 28 | get_state() -> 29 | case catch global:send(test_saved_state, {get_state, self()}) of 30 | {'EXIT',{badarg, _}} -> 31 | {error, no_record}; 32 | _ -> 33 | receive 34 | {ok, State} -> 35 | State 36 | after 1000 -> 37 | {error, timeout} 38 | end 39 | end. 40 | 41 | set_state(State) -> 42 | spawn(fun() -> wrapper(State) end), 43 | ok. 44 | 45 | wrapper(State) -> 46 | case global:register_name(test_saved_state, self()) of 47 | no -> 48 | global:send(test_saved_state, {set_state, State}); 49 | yes -> 50 | recv(State) 51 | end. 52 | 53 | recv(State) -> 54 | receive 55 | {set_state, NewState} -> 56 | recv(NewState); 57 | {get_state, Pid} -> 58 | Pid ! {ok, State}, 59 | recv(State) 60 | end. 61 | -------------------------------------------------------------------------------- /elib/ernie.hrl: -------------------------------------------------------------------------------- 1 | -record(state, {lsock = undefined, % the listen socket 2 | listen = true, % whether to listen for new connections 3 | hq = queue:new(), % high priority queue 4 | lq = queue:new(), % low priority queue 5 | count = 0, % total request count 6 | zcount = 0, % total completed request count 7 | map = undefined}). % module map. tuples of {Mod, Id} 8 | 9 | -record(request, {sock = undefined, % connection socket 10 | log = undefined, % log information 11 | infos = [], % list of info binaries 12 | action = undefined, % action binary 13 | priority = high}). % priority [ high | low ] 14 | 15 | -record(log, {taccept = erlang:now(), % time that connection was accepted 16 | tprocess = erlang:now(), % time that processing started 17 | tdone = erlang:now(), % time that processing and response is done 18 | hq = 0, % size of high queue at acceptance 19 | lq = 0, % size of low queue at acceptance 20 | type = unk}). % type [ unk | nat | ext ] -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | = 2.5.2 / 2010-11-23 2 | * Bug Fixes 3 | * Don't do extra logging work unless necessary 4 | 5 | = 2.5.1 / 2010-11-22 6 | * Bug Fixes 7 | * Prevent ernie from trying to start in ernie bin 8 | 9 | = 2.5.0 / 2010-11-19 10 | * Major Enhancements 11 | * Enable multi-node mode (#11) 12 | * Bug Fixes 13 | * Properly determine whether function or module is missing. 14 | * Spawn a process for receive_term to keep acceptor non-blocking. 15 | 16 | = 2.4.0 / 2010-05-21 17 | * Minor Additions 18 | * Add -E cli option for setting extra Erlang VM options 19 | 20 | = 2.3.0 / 2010-05-17 21 | * Minor Additions 22 | * Add 'halt' command for gracefully shutting down 23 | 24 | = 2.2.1 / 2010-05-13 25 | * Minor Changes 26 | * Increase TCP backlog from 5 to 128 27 | 28 | = 2.2.0 / 2010-03-12 29 | * Minor Additions 30 | * Set procline for external Ruby handlers 31 | 32 | = 2.1.0 / 2010-02-20 33 | * Major Additions 34 | * Add access logging 35 | 36 | = 2.0.0 / 2010-02-16 37 | * Major Changes 38 | * Use configuration file for defining handlers 39 | * Add Native Erlang modules 40 | * Abstract handler logic to support handlers in any language 41 | * Add High/Low connection queues 42 | * Remove Ruby DSL (must use Ernie.expose now) 43 | 44 | = 1.3.0 / 2009-11-30 45 | * API Additions 46 | * Add loglevel for setting log level 47 | * Add Ernie.auto_start bool 48 | * Major changes 49 | * Better logging granularity 50 | 51 | = 1.2.0 / 2009-11-23 52 | * API Additions 53 | * Add Ernie.expose 54 | * Internal Changes 55 | * Remove 15s internal timeout 56 | 57 | = 1.1.0 / 2009-10-28 58 | * Major changes 59 | * Remove dependency on Erlectricity 60 | * Simplify processing loop 61 | 62 | = 1.0.0 / 2009-10-19 63 | * No Changes. Production ready! 64 | 65 | = 0.4.0 / 2009-10-08 66 | * Major changes 67 | * Convert to use BERT gem. -------------------------------------------------------------------------------- /elib/bert.erl: -------------------------------------------------------------------------------- 1 | %%% See http://github.com/mojombo/bert.erl for documentation. 2 | %%% MIT License - Copyright (c) 2009 Tom Preston-Werner 3 | 4 | -module(bert). 5 | -version('1.1.0'). 6 | -author("Tom Preston-Werner"). 7 | 8 | -export([encode/1, decode/1]). 9 | 10 | -ifdef(TEST). 11 | -include("test/bert_test.erl"). 12 | -endif. 13 | 14 | %%--------------------------------------------------------------------------- 15 | %% Public API 16 | 17 | -spec encode(term()) -> binary(). 18 | 19 | encode(Term) -> 20 | term_to_binary(encode_term(Term)). 21 | 22 | -spec decode(binary()) -> term(). 23 | 24 | decode(Bin) -> 25 | decode_term(binary_to_term(Bin)). 26 | 27 | %%--------------------------------------------------------------------------- 28 | %% Encode 29 | 30 | -spec encode_term(term()) -> term(). 31 | 32 | encode_term(Term) -> 33 | case Term of 34 | [] -> {bert, nil}; 35 | true -> {bert, true}; 36 | false -> {bert, false}; 37 | Dict when is_record(Term, dict, 8) -> 38 | {bert, dict, dict:to_list(Dict)}; 39 | List when is_list(Term) -> 40 | lists:map((fun encode_term/1), List); 41 | Tuple when is_tuple(Term) -> 42 | TList = tuple_to_list(Tuple), 43 | TList2 = lists:map((fun encode_term/1), TList), 44 | list_to_tuple(TList2); 45 | _Else -> Term 46 | end. 47 | 48 | %%--------------------------------------------------------------------------- 49 | %% Decode 50 | 51 | -spec decode_term(term()) -> term(). 52 | 53 | decode_term(Term) -> 54 | case Term of 55 | {bert, nil} -> []; 56 | {bert, true} -> true; 57 | {bert, false} -> false; 58 | {bert, dict, Dict} -> 59 | dict:from_list(Dict); 60 | {bert, Other} -> 61 | {bert, Other}; 62 | List when is_list(Term) -> 63 | lists:map((fun decode_term/1), List); 64 | Tuple when is_tuple(Term) -> 65 | TList = tuple_to_list(Tuple), 66 | TList2 = lists:map((fun decode_term/1), TList), 67 | list_to_tuple(TList2); 68 | _Else -> Term 69 | end. -------------------------------------------------------------------------------- /ernie.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.specification_version = 2 if s.respond_to? :specification_version= 3 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 4 | s.rubygems_version = '1.3.6' 5 | 6 | s.name = 'ernie' 7 | s.version = '2.5.2' 8 | s.date = '2010-11-23' 9 | s.rubyforge_project = 'ernie' 10 | 11 | s.summary = "Ernie is a BERT-RPC server implementation." 12 | s.description = "Ernie is an Erlang/Ruby hybrid BERT-RPC server implementation packaged as a gem." 13 | 14 | s.authors = ["Tom Preston-Werner"] 15 | s.email = 'tom@mojombo.com' 16 | s.homepage = 'http://github.com/mojombo/ernie' 17 | 18 | s.require_paths = %w[lib] 19 | 20 | s.extensions = ["ext/extconf.rb"] 21 | 22 | s.executables = ["ernie"] 23 | s.default_executable = 'ernie' 24 | 25 | s.rdoc_options = ["--charset=UTF-8"] 26 | s.extra_rdoc_files = %w[LICENSE README.md] 27 | 28 | s.add_dependency('bert', [">= 1.1.0"]) 29 | s.add_dependency('bertrpc', [">= 1.0.0"]) 30 | 31 | s.add_development_dependency('shoulda', [">= 2.11.3", "< 3.0.0"]) 32 | 33 | # = MANIFEST = 34 | s.files = %w[ 35 | History.txt 36 | LICENSE 37 | README.md 38 | Rakefile 39 | bin/ernie 40 | contrib/ebench.erl 41 | ebin/ernie_server_app.app 42 | elib/asset_pool.erl 43 | elib/asset_pool_sup.erl 44 | elib/bert.erl 45 | elib/ernie.hrl 46 | elib/ernie_access_logger.erl 47 | elib/ernie_access_logger_sup.erl 48 | elib/ernie_admin.erl 49 | elib/ernie_config.erl 50 | elib/ernie_native.erl 51 | elib/ernie_server.erl 52 | elib/ernie_server_app.erl 53 | elib/ernie_server_sup.erl 54 | elib/logger.erl 55 | elib/logger_sup.erl 56 | elib/port_wrapper.erl 57 | ernie.gemspec 58 | examples/example.cfg 59 | examples/example.config 60 | examples/ext.erl 61 | examples/ext.rb 62 | examples/nat.erl 63 | ext/Makefile 64 | ext/extconf.rb 65 | lib/ernie.rb 66 | test/helper.rb 67 | test/load.rb 68 | test/sample/ext.rb 69 | test/sample/intTest.erl 70 | test/sample/sample.cfg 71 | test/test_ernie.rb 72 | test/test_ernie_server.rb 73 | ] 74 | # = MANIFEST = 75 | 76 | s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ } 77 | end 78 | -------------------------------------------------------------------------------- /contrib/ebench.erl: -------------------------------------------------------------------------------- 1 | % erlc *.erl && erl ebench.beam -run ebench start 10000 20 ext add 2 | 3 | -module(ebench). 4 | -export([start/1]). 5 | 6 | start([Ni, Ci, Modi, Funi]) -> 7 | Nt = list_to_integer(Ni), 8 | C = list_to_integer(Ci), 9 | Mod = list_to_atom(Modi), 10 | Fun = list_to_atom(Funi), 11 | N = round(Nt / C), 12 | T0 = erlang:now(), 13 | Waiter = spawn(fun() -> wait(T0, N * C) end), 14 | spawner(Waiter, N, C, Mod, Fun). 15 | 16 | spawner(_Waiter, _N, 0, _Mod, _Fun) -> 17 | ok; 18 | spawner(Waiter, N, C, Mod, Fun) -> 19 | spawn(fun() -> loop(Waiter, N, Mod, Fun) end), 20 | spawner(Waiter, N, C - 1, Mod, Fun). 21 | 22 | % X is the total number of responses to wait for 23 | wait(T0, XTotal, 0) -> 24 | T1 = erlang:now(), 25 | Diff = timer:now_diff(T1, T0), 26 | Mean = Diff / XTotal, 27 | io:format("~p requests completed in ~.2fs~n", [XTotal, Diff / 1000000]), 28 | io:format("Mean request time: ~.2fms (~.2f r/s)~n", [Mean / 1000, XTotal / (Diff / 1000000)]), 29 | init:stop(); 30 | wait(T0, XTotal, X) -> 31 | receive 32 | done -> wait(T0, XTotal, X - 1) 33 | end. 34 | 35 | wait(T0, X) -> 36 | wait(T0, X, X). 37 | 38 | loop(_Waiter, 0, _Mod, _Fun) -> 39 | ok; 40 | loop(Waiter, N, Mod, Fun) -> 41 | hit(Waiter, Mod, Fun), 42 | loop(Waiter, N - 1, Mod, Fun). 43 | 44 | hit(Waiter, Mod, Fun) -> 45 | % io:format("outgoing!~n", []), 46 | Host = "localhost", 47 | case gen_tcp:connect(Host, 8000, [binary, {packet, 4}]) of 48 | {ok, Sock} -> process(Waiter, Mod, Fun, Sock); 49 | Any -> 50 | io:format("Unable to establish connection: ~p~n", [Any]), 51 | Waiter ! done 52 | end. 53 | 54 | process(Waiter, Mod, Fun, Sock) -> 55 | % Info = term_to_binary({info, priority, [low]}), 56 | % ok = gen_tcp:send(Sock, Info), 57 | Request = term_to_binary({call, Mod, Fun, args(Fun)}), 58 | ok = gen_tcp:send(Sock, Request), 59 | receive 60 | {tcp, _Port, Reply} -> 61 | % io:format("~p~n", [Reply]), 62 | Res = res(Fun), 63 | {reply, Res} = binary_to_term(Reply); 64 | {tcp_closed, Port} -> 65 | io:format("Connection closed after sending data: ~p~n", [Port]); 66 | Any -> 67 | io:format("Unexpected message: ~p~n", [Any]) 68 | end, 69 | Waiter ! done, 70 | ok = gen_tcp:close(Sock). 71 | 72 | args(add) -> [1, 2]; 73 | args(fib) -> [20]. 74 | 75 | res(add) -> 3; 76 | res(fib) -> 10946. -------------------------------------------------------------------------------- /elib/port_wrapper.erl: -------------------------------------------------------------------------------- 1 | -module(port_wrapper). 2 | -export([wrap/1, wrap/2, wrap_link/1, wrap_link/2, send/2, shutdown/1, close/1, rpc/2]). 3 | 4 | wrap(Command) -> 5 | spawn(fun() -> process_flag(trap_exit, true), Port = create_port(Command), loop(Port, infinity, Command) end). 6 | wrap(Command, Timeout) -> 7 | spawn(fun() -> process_flag(trap_exit, true), Port = create_port(Command), loop(Port, Timeout, Command) end). 8 | 9 | wrap_link(Command) -> 10 | spawn_link(fun() -> process_flag(trap_exit, true), Port = create_port(Command), link(Port), loop(Port, infinity, Command) end). 11 | wrap_link(Command, Timeout) -> 12 | spawn_link(fun() -> process_flag(trap_exit, true), Port = create_port(Command), link(Port), loop(Port, Timeout, Command) end). 13 | 14 | rpc(WrappedPort, Message) -> 15 | send(WrappedPort, Message), 16 | receive 17 | {WrappedPort, Result} -> {ok, Result} 18 | end. 19 | 20 | send(WrappedPort, Message) -> 21 | WrappedPort ! {self(), {command, Message}}, 22 | WrappedPort. 23 | 24 | shutdown(WrappedPort) -> 25 | WrappedPort ! shutdown, 26 | true. 27 | 28 | close(WrappedPort) -> 29 | WrappedPort ! noose, 30 | true. 31 | 32 | create_port(Command) -> 33 | open_port({spawn, Command}, [{packet, 4}, nouse_stdio, exit_status, binary]). 34 | 35 | loop(Port, Timeout, Command) -> 36 | receive 37 | noose -> 38 | port_close(Port), 39 | noose; 40 | shutdown -> 41 | port_close(Port), 42 | exit(shutdown); 43 | {Source, {command, Message}} -> 44 | Port ! {self(), {command, Message}}, 45 | receive 46 | {Port, {data, Result}} -> 47 | Source ! {self(), Result} 48 | after Timeout -> 49 | error_logger:error_msg("Port Wrapper ~p timed out in mid operation (~p)!~n", [self(),Message]), 50 | % We timed out, which means we need to close and then restart the port 51 | port_close(Port), % Should SIGPIPE the child. 52 | exit(timed_out) 53 | end, 54 | loop(Port,Timeout,Command); 55 | {Port, {exit_status, _Code}} -> 56 | % Hard and Unanticipated Crash 57 | error_logger:error_msg( "Port closed! ~p~n", [Port] ), 58 | exit({error, _Code}); 59 | {'EXIT',_Pid,shutdown} -> 60 | port_close(Port), 61 | exit(shutdown); 62 | Any -> 63 | error_logger:warning_msg("PortWrapper ~p got unexpected message: ~p~n", [self(), Any]), 64 | loop(Port, Timeout, Command) 65 | end. -------------------------------------------------------------------------------- /elib/ernie_admin.erl: -------------------------------------------------------------------------------- 1 | -module(ernie_admin). 2 | -export([process/4]). 3 | -include_lib("ernie.hrl"). 4 | 5 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 6 | % Process entry point 7 | 8 | process(Sock, reload_handlers, _Args, State) -> 9 | spawn(fun() -> process_reload_assets(Sock, State) end), 10 | State; 11 | process(Sock, halt, _Args, State) -> 12 | process_halt(Sock, State), 13 | State#state{listen = false}; 14 | process(Sock, stats, _Args, State) -> 15 | spawn(fun() -> process_stats(Sock, State) end), 16 | State; 17 | process(Sock, _Fun, _Args, State) -> 18 | gen_tcp:send(Sock, term_to_binary({reply, <<"Admin function not supported.">>})), 19 | ok = gen_tcp:close(Sock), 20 | State. 21 | 22 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 23 | % Reload handlers 24 | 25 | process_reload_assets(Sock, State) -> 26 | lists:map((fun reload/1), State#state.map), 27 | gen_tcp:send(Sock, term_to_binary({reply, <<"Handlers reloaded.">>})), 28 | ok = gen_tcp:close(Sock). 29 | 30 | reload({_Mod, native}) -> 31 | ok; 32 | reload({_Mod, Pid}) -> 33 | asset_pool:reload_assets(Pid). 34 | 35 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 36 | % Halt 37 | 38 | process_halt(Sock, State) -> 39 | gen_tcp:send(Sock, term_to_binary({reply, <<"Halting.">>})), 40 | ok = gen_tcp:close(Sock), 41 | gen_tcp:close(State#state.lsock), 42 | case State#state.count =:= State#state.zcount of 43 | true -> halt(); 44 | false -> ok 45 | end. 46 | 47 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 48 | % Stats 49 | 50 | process_stats(Sock, State) -> 51 | CountString = stat(count, State), 52 | ZCountString = stat(zcount, State), 53 | IdleWorkersString = stat(idle, State), 54 | QueueLengthString = stat(queue, State), 55 | StatString = list_to_binary([CountString, ZCountString, IdleWorkersString, QueueLengthString]), 56 | Data = term_to_binary({reply, StatString}), 57 | gen_tcp:send(Sock, Data), 58 | ok = gen_tcp:close(Sock). 59 | 60 | stat(count, State) -> 61 | Count = State#state.count, 62 | list_to_binary([<<"connections.total=">>, integer_to_list(Count), <<"\n">>]); 63 | stat(zcount, State) -> 64 | ZCount = State#state.zcount, 65 | list_to_binary([<<"connections.completed=">>, integer_to_list(ZCount), <<"\n">>]); 66 | stat(idle, State) -> 67 | IdleMap = lists:map((fun idle/1), State#state.map), 68 | list_to_binary(IdleMap); 69 | stat(queue, State) -> 70 | HighQueueLength = queue:len(State#state.hq), 71 | LowQueueLength = queue:len(State#state.lq), 72 | list_to_binary([<<"queue.high=">>, integer_to_list(HighQueueLength), <<"\n">>, 73 | <<"queue.low=">>, integer_to_list(LowQueueLength), <<"\n">>]). 74 | 75 | idle({Mod, native}) -> 76 | list_to_binary([<<"workers.idle.">>, atom_to_list(Mod), <<"=native\n">>]); 77 | idle({Mod, Pid}) -> 78 | IdleCount = integer_to_list(asset_pool:idle_worker_count(Pid)), 79 | list_to_binary([<<"workers.idle.">>, atom_to_list(Mod), <<"=">>, IdleCount, <<"\n">>]). 80 | -------------------------------------------------------------------------------- /bin/ernie: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift(File.join(File.dirname(__FILE__), *%w[.. lib])) 4 | ERNIE_ROOT = File.join(File.dirname(__FILE__), *%w[..]) 5 | 6 | DEFAULT_ERLANG_CODEPATHS = %w[ebin] 7 | DEFAULT_PORT = 8000 8 | 9 | def rel(path) 10 | File.join(ERNIE_ROOT, path) 11 | end 12 | 13 | def code_paths 14 | DEFAULT_ERLANG_CODEPATHS.map {|n| "-pz #{rel(n)}" }.join(" ") + " \\" 15 | end 16 | 17 | require 'optparse' 18 | require 'pp' 19 | require 'yaml' 20 | require 'ernie' 21 | 22 | Ernie.auto_start = false 23 | 24 | help = < Start an Ernie server. 32 | reload-handlers Gracefully reload all of the the ruby handlers 33 | and use the new code for all subsequent requests. 34 | stats Print usage statistics. 35 | halt Gracefully halt the server. 36 | 37 | Options: 38 | HELP 39 | 40 | options = {} 41 | OptionParser.new do |opts| 42 | opts.banner = help 43 | opts.version = Ernie::VERSION 44 | 45 | opts.on("-c CONFIG", "--config CONFIG", "Config file") do |x| 46 | options[:config] = x 47 | end 48 | 49 | opts.on("-p PORT", "--port PORT", "Port") do |x| 50 | options[:port] = x 51 | end 52 | 53 | opts.on("-l LOGLEVEL", "--log-level LOGLEVEL", "Log level (0-4)") do |x| 54 | options[:log_level] = x 55 | end 56 | 57 | opts.on("-a LOGFILE", "--access-log LOGFILE", "Access log file") do |x| 58 | options[:access_log] = x 59 | end 60 | 61 | opts.on("-d", "--detached", "Run as a daemon") do 62 | options[:detached] = true 63 | end 64 | 65 | opts.on("--name NAME", "Erlang proccess name") do |x| 66 | options[:name] = x 67 | end 68 | 69 | opts.on("--sname NAME", "Erlang short proccess name") do |x| 70 | options[:sname] = x 71 | end 72 | 73 | opts.on("-P", "--pidfile PIDFILE", "Location to write pid file.") do |x| 74 | options[:pidfile] = x 75 | end 76 | 77 | opts.on("-E", "--erlang ERLANG_OPTIONS", "Additional parameters to Erlang VM") do |x| 78 | options[:erl_options] = x 79 | end 80 | end.parse! 81 | 82 | if command = ARGV[0] 83 | if !%w{reload-handlers stats halt}.include?(command) 84 | puts "Invalid command. Valid commands are:" 85 | puts " reload-handlers" 86 | puts " stats" 87 | puts " halt" 88 | exit(1) 89 | end 90 | 91 | require 'rubygems' 92 | require 'bertrpc' 93 | port = options[:port] || DEFAULT_PORT 94 | svc = BERTRPC::Service.new('localhost', port) 95 | puts svc.call.__admin__.send(command.gsub(/-/, '_')) 96 | else 97 | if !options[:config] 98 | puts "A config file must be specified: ernie -c /path/to/config.yml" 99 | exit(1) 100 | end 101 | 102 | config = options[:config] 103 | port = options[:port] || DEFAULT_PORT 104 | log_level = options[:log_level] || 2 105 | pidfile = options[:pidfile] ? "-ernie_server_app pidfile \"'#{options[:pidfile]}'\"" : '' 106 | detached = options[:detached] ? '-detached' : '' 107 | access_log = options[:access_log] ? "-ernie_server_app access_log '\"#{options[:access_log]}\"'" : '' 108 | name = options[:name] ? "-name #{options[:name]}" : '' 109 | sname = options[:sname] ? "-sname #{options[:sname]}" : '' 110 | erl_options = options[:erl_options] 111 | 112 | cmd = %Q{erl -boot start_sasl \ 113 | #{name} \ 114 | #{sname} \ 115 | #{detached} \ 116 | +Bc \ 117 | +K true \ 118 | -smp enable \ 119 | #{code_paths} 120 | #{pidfile} \ 121 | #{access_log} \ 122 | -ernie_server_app port #{port} \ 123 | -ernie_server_app config '"#{config}"' \ 124 | -ernie_server_app log_level #{log_level} \ 125 | -run ernie_server_app boot \ 126 | #{erl_options}}.squeeze(' ') 127 | puts cmd 128 | STDOUT.flush 129 | exec(cmd) 130 | end 131 | -------------------------------------------------------------------------------- /elib/logger.erl: -------------------------------------------------------------------------------- 1 | -module(logger). 2 | -behaviour(gen_server). 3 | 4 | %% api 5 | -export([start_link/1, start/1, set_log_level/1, debug/2, info/2, warn/2, error/2, fatal/2]). 6 | 7 | %% gen_server callbacks 8 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 9 | terminate/2, code_change/3]). 10 | 11 | -record(state, {log_level = undefined}). 12 | 13 | %%==================================================================== 14 | %% API 15 | %%==================================================================== 16 | 17 | start_link(Args) -> 18 | gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []). 19 | 20 | start(Args) -> 21 | gen_server:start({local, ?MODULE}, ?MODULE, Args, []). 22 | 23 | set_log_level(Level) -> 24 | gen_server:call(?MODULE, {set_log_level, Level}). 25 | 26 | debug(Msg, Args) -> 27 | gen_server:cast(?MODULE, {debug, Msg, Args}). 28 | 29 | info(Msg, Args) -> 30 | gen_server:cast(?MODULE, {info, Msg, Args}). 31 | 32 | warn(Msg, Args) -> 33 | gen_server:cast(?MODULE, {warn, Msg, Args}). 34 | 35 | error(Msg, Args) -> 36 | gen_server:cast(?MODULE, {error, Msg, Args}). 37 | 38 | fatal(Msg, Args) -> 39 | gen_server:cast(?MODULE, {fatal, Msg, Args}). 40 | 41 | %%==================================================================== 42 | %% gen_server callbacks 43 | %%==================================================================== 44 | 45 | %%-------------------------------------------------------------------- 46 | %% Function: init(Args) -> {ok, State} | 47 | %% {ok, State, Timeout} | 48 | %% ignore | 49 | %% {stop, Reason} 50 | %% Description: Initiates the server 51 | %%-------------------------------------------------------------------- 52 | init([LogLevel]) -> 53 | error_logger:info_msg("~p starting~n", [?MODULE]), 54 | {ok, #state{log_level = LogLevel}}. 55 | 56 | %%-------------------------------------------------------------------- 57 | %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | 58 | %% {reply, Reply, State, Timeout} | 59 | %% {noreply, State} | 60 | %% {noreply, State, Timeout} | 61 | %% {stop, Reason, Reply, State} | 62 | %% {stop, Reason, State} 63 | %% Description: Handling call messages 64 | %%-------------------------------------------------------------------- 65 | handle_call({set_log_level, Level}, _From, State) -> 66 | {reply, ok, State#state{log_level = Level}}; 67 | handle_call(_Request, _From, State) -> 68 | {reply, ok, State}. 69 | 70 | %%-------------------------------------------------------------------- 71 | %% Function: handle_cast(Msg, State) -> {noreply, State} | 72 | %% {noreply, State, Timeout} | 73 | %% {stop, Reason, State} 74 | %% Description: Handling cast messages 75 | %%-------------------------------------------------------------------- 76 | handle_cast({debug, Msg, Args}, State) -> 77 | log(State#state.log_level, 4, Msg, Args), 78 | {noreply, State}; 79 | handle_cast({info, Msg, Args}, State) -> 80 | log(State#state.log_level, 3, Msg, Args), 81 | {noreply, State}; 82 | handle_cast({warn, Msg, Args}, State) -> 83 | log(State#state.log_level, 2, Msg, Args), 84 | {noreply, State}; 85 | handle_cast({error, Msg, Args}, State) -> 86 | log(State#state.log_level, 1, Msg, Args), 87 | {noreply, State}; 88 | handle_cast({fatal, Msg, Args}, State) -> 89 | log(State#state.log_level, 0, Msg, Args), 90 | {noreply, State}; 91 | handle_cast(_Msg, State) -> {noreply, State}. 92 | 93 | handle_info(Msg, State) -> 94 | error_logger:error_msg("Unexpected message: ~p~n", [Msg]), 95 | {noreply, State}. 96 | 97 | terminate(_Reason, _State) -> ok. 98 | code_change(_OldVersion, State, _Extra) -> {ok, State}. 99 | 100 | %%==================================================================== 101 | %% Internal 102 | %%==================================================================== 103 | 104 | log(SystemLogLevel, MessageLogLevel, Message, Args) -> 105 | case SystemLogLevel >= MessageLogLevel of 106 | false -> ok; 107 | true -> io:format(Message, Args) 108 | end. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'date' 4 | 5 | ############################################################################# 6 | # 7 | # Helper functions 8 | # 9 | ############################################################################# 10 | 11 | def name 12 | @name ||= Dir['*.gemspec'].first.split('.').first 13 | end 14 | 15 | def version 16 | line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/] 17 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1] 18 | end 19 | 20 | def date 21 | Date.today.to_s 22 | end 23 | 24 | def rubyforge_project 25 | name 26 | end 27 | 28 | def gemspec_file 29 | "#{name}.gemspec" 30 | end 31 | 32 | def gem_file 33 | "#{name}-#{version}.gem" 34 | end 35 | 36 | def replace_header(head, header_name) 37 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"} 38 | end 39 | 40 | ############################################################################# 41 | # 42 | # Standard tasks 43 | # 44 | ############################################################################# 45 | 46 | task :default => :test 47 | 48 | require 'rake/testtask' 49 | Rake::TestTask.new(:test) do |test| 50 | test.libs << 'lib' << 'test' 51 | test.pattern = 'test/**/test_*.rb' 52 | test.verbose = true 53 | end 54 | 55 | desc "Generate RCov test coverage and open in your browser" 56 | task :coverage do 57 | require 'rcov' 58 | sh "rm -fr coverage" 59 | sh "rcov test/test_*.rb" 60 | sh "open coverage/index.html" 61 | end 62 | 63 | require 'rake/rdoctask' 64 | Rake::RDocTask.new do |rdoc| 65 | rdoc.rdoc_dir = 'rdoc' 66 | rdoc.title = "#{name} #{version}" 67 | rdoc.rdoc_files.include('README*') 68 | rdoc.rdoc_files.include('lib/**/*.rb') 69 | end 70 | 71 | desc "Open an irb session preloaded with this library" 72 | task :console do 73 | sh "irb -rubygems -r ./lib/#{name}.rb" 74 | end 75 | 76 | ############################################################################# 77 | # 78 | # Custom tasks (add your own tasks here) 79 | # 80 | ############################################################################# 81 | 82 | task :ebuild do 83 | ERLC_TEST_FLAGS = "" 84 | ERLC_FLAGS = "-o ../ebin" 85 | cd "elib" 86 | sh "erlc #{ERLC_FLAGS} #{ERLC_TEST_FLAGS} #{Dir["**/*.erl"].join(" ")}" 87 | end 88 | 89 | ############################################################################# 90 | # 91 | # Packaging tasks 92 | # 93 | ############################################################################# 94 | 95 | desc "Create tag v#{version} and build and push #{gem_file} to Rubygems" 96 | task :release => :build do 97 | unless `git branch` =~ /^\* master$/ 98 | puts "You must be on the master branch to release!" 99 | exit! 100 | end 101 | sh "git commit --allow-empty -a -m 'Release #{version}'" 102 | sh "git tag v#{version}" 103 | sh "git push origin master" 104 | sh "git push origin v#{version}" 105 | sh "gem push pkg/#{name}-#{version}.gem" 106 | end 107 | 108 | desc "Build #{gem_file} into the pkg directory" 109 | task :build => :gemspec do 110 | sh "mkdir -p pkg" 111 | sh "gem build #{gemspec_file}" 112 | sh "mv #{gem_file} pkg" 113 | end 114 | 115 | desc "Generate #{gemspec_file}" 116 | task :gemspec => :validate do 117 | # read spec file and split out manifest section 118 | spec = File.read(gemspec_file) 119 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 120 | 121 | # replace name version and date 122 | replace_header(head, :name) 123 | replace_header(head, :version) 124 | replace_header(head, :date) 125 | #comment this out if your rubyforge_project has a different name 126 | replace_header(head, :rubyforge_project) 127 | 128 | # determine file list from git ls-files 129 | files = `git ls-files`. 130 | split("\n"). 131 | sort. 132 | reject { |file| file =~ /^\./ }. 133 | reject { |file| file =~ /^(rdoc|pkg)/ }. 134 | map { |file| " #{file}" }. 135 | join("\n") 136 | 137 | # piece file back together and write 138 | manifest = " s.files = %w[\n#{files}\n ]\n" 139 | spec = [head, manifest, tail].join(" # = MANIFEST =\n") 140 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 141 | puts "Updated #{gemspec_file}" 142 | end 143 | 144 | desc "Validate #{gemspec_file}" 145 | task :validate do 146 | libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"] 147 | unless libfiles.empty? 148 | puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir." 149 | exit! 150 | end 151 | unless Dir['VERSION*'].empty? 152 | puts "A `VERSION` file at root level violates Gem best practices." 153 | exit! 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /elib/asset_pool.erl: -------------------------------------------------------------------------------- 1 | -module(asset_pool). 2 | -behaviour(gen_server). 3 | 4 | %% api 5 | -export([start_link/2, lease/1, return/2, reload_assets/1, idle_worker_count/1]). 6 | 7 | %% gen_server callbacks 8 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 9 | terminate/2, code_change/3]). 10 | 11 | -record(state, {assets = undefined, 12 | count = undefined, 13 | handler = undefined, 14 | token = undefined}). 15 | 16 | %%==================================================================== 17 | %% API 18 | %%==================================================================== 19 | 20 | start_link(Handler, Count) -> 21 | gen_server:start_link(?MODULE, [Handler, Count], []). 22 | 23 | lease(Pid) -> 24 | gen_server:call(Pid, lease). 25 | 26 | return(Pid, Asset) -> 27 | gen_server:call(Pid, {return, Asset}). 28 | 29 | reload_assets(Pid) -> 30 | gen_server:call(Pid, reload_assets). 31 | 32 | idle_worker_count(Pid) -> 33 | gen_server:call(Pid, idle_worker_count). 34 | 35 | %%==================================================================== 36 | %% gen_server callbacks 37 | %%==================================================================== 38 | 39 | %%-------------------------------------------------------------------- 40 | %% Function: init(Args) -> {ok, State} | 41 | %% {ok, State, Timeout} | 42 | %% ignore | 43 | %% {stop, Reason} 44 | %% Description: Initiates the server 45 | %%-------------------------------------------------------------------- 46 | init([Handler, Count]) -> 47 | process_flag(trap_exit, true), 48 | error_logger:info_msg("~p starting~n", [?MODULE]), 49 | Token = make_ref(), 50 | Assets = start_handlers(Count, Handler, Token), 51 | logger:debug("Assets = ~p~n", [Assets]), 52 | {ok, #state{assets = Assets, count = Count, handler = Handler, token = Token}}. 53 | 54 | %%-------------------------------------------------------------------- 55 | %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | 56 | %% {reply, Reply, State, Timeout} | 57 | %% {noreply, State} | 58 | %% {noreply, State, Timeout} | 59 | %% {stop, Reason, Reply, State} | 60 | %% {stop, Reason, State} 61 | %% Description: Handling call messages 62 | %%-------------------------------------------------------------------- 63 | handle_call(lease, _From, State) -> 64 | logger:debug("Leasing...~n", []), 65 | Token = State#state.token, 66 | case queue:out(State#state.assets) of 67 | {{value, Asset}, Assets2} -> 68 | {asset, Port, AssetToken} = Asset, 69 | case AssetToken =:= Token of 70 | false -> 71 | port_wrapper:close(Port), 72 | Handler = State#state.handler, 73 | NewAsset = create_asset(Handler, Token); 74 | true -> 75 | NewAsset = Asset 76 | end, 77 | {reply, {ok, NewAsset}, State#state{assets = Assets2}}; 78 | {empty, _Assets2} -> 79 | {reply, empty, State} 80 | end; 81 | handle_call({return, Asset}, _From, State) -> 82 | Token = State#state.token, 83 | {asset, Port, AssetToken} = Asset, 84 | case AssetToken =:= Token of 85 | false -> 86 | port_wrapper:close(Port), 87 | Handler = State#state.handler, 88 | NewAsset = create_asset(Handler, Token); 89 | true -> 90 | NewAsset = Asset 91 | end, 92 | Assets2 = queue:in(NewAsset, State#state.assets), 93 | {reply, ok, State#state{assets = Assets2}}; 94 | handle_call(reload_assets, _From, State) -> 95 | Token = make_ref(), 96 | {reply, ok, State#state{token = Token}}; 97 | handle_call(idle_worker_count, _From, State) -> 98 | WorkerCount = queue:len(State#state.assets), 99 | {reply, WorkerCount, State}; 100 | handle_call(_Request, _From, State) -> 101 | {reply, ok, State}. 102 | 103 | %%-------------------------------------------------------------------- 104 | %% Function: handle_cast(Msg, State) -> {noreply, State} | 105 | %% {noreply, State, Timeout} | 106 | %% {stop, Reason, State} 107 | %% Description: Handling cast messages 108 | %%-------------------------------------------------------------------- 109 | handle_cast(_Msg, State) -> {noreply, State}. 110 | 111 | handle_info({'EXIT', _Pid, normal}, State) -> 112 | {noreply, State}; 113 | handle_info({'EXIT', Pid, Error}, State) -> 114 | error_logger:error_msg("Port ~p closed with ~p, restarting port...~n", [Pid, Error]), 115 | ValidAssets = queue:filter(fun(Item) -> {asset, A, _T} = Item, A =/= Pid end, State#state.assets), 116 | Handler = State#state.handler, 117 | Token = State#state.token, 118 | NewAsset = create_asset(Handler, Token), 119 | Assets = queue:in(NewAsset, ValidAssets), 120 | {noreply, State#state{assets = Assets}}; 121 | handle_info(Msg, State) -> 122 | error_logger:error_msg("Unexpected message: ~p~n", [Msg]), 123 | {noreply, State}. 124 | 125 | terminate(_Reason, _State) -> ok. 126 | code_change(_OldVersion, State, _Extra) -> {ok, State}. 127 | 128 | %%==================================================================== 129 | %% Internal 130 | %%==================================================================== 131 | 132 | start_handlers(Count, Handler, Token) -> 133 | start_handlers(queue:new(), Count, Handler, Token). 134 | 135 | start_handlers(Assets, 0, _Handler, _Token) -> 136 | Assets; 137 | start_handlers(Assets, Count, Handler, Token) -> 138 | Asset = create_asset(Handler, Token), 139 | Assets2 = queue:in(Asset, Assets), 140 | start_handlers(Assets2, Count - 1, Handler, Token). 141 | 142 | create_asset(Handler, Token) -> 143 | Len = length(Handler), 144 | case Len > 150 of 145 | true -> Cmd = Handler; 146 | false -> Cmd = lists:flatten(Handler ++ " --procline " ++ pad(150 - Len - 12)) 147 | end, 148 | io:format("~p~n", [Cmd]), 149 | {asset, port_wrapper:wrap_link(Cmd), Token}. 150 | 151 | pad(Size) -> 152 | pad(Size, []). 153 | pad(0, Acc) -> 154 | Acc; 155 | pad(Size, Acc) -> 156 | pad(Size - 1, ["x" | Acc]). -------------------------------------------------------------------------------- /test/test_ernie_server.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | PORT = 27118 4 | 5 | class ErnieServerTest < Test::Unit::TestCase 6 | 7 | # global setup 8 | def setup 9 | @servers ||= [] 10 | Dir.chdir(ERNIE_ROOT) 11 | `erlc -o test/sample #{ERNIE_ROOT}/test/sample/intTest.erl` 12 | Signal.trap("INT") do 13 | puts "Shutting Down" 14 | shutdown_servers 15 | teardown 16 | exit 17 | end 18 | end 19 | 20 | def teardown 21 | `rm test/sample/intTest.beam` 22 | end 23 | 24 | context "An Ernie Server" do 25 | setup do 26 | start_server 27 | end 28 | 29 | context "call" do 30 | should "handle zeronary" do 31 | assert_equal :foo, svc.call.ext.zeronary 32 | assert_equal :foo, svc.call.intTest.zeronary 33 | end 34 | 35 | should "handle unary" do 36 | assert_equal 5, svc.call.ext.unary(5) 37 | assert_equal 5, svc.call.intTest.unary(5) 38 | end 39 | 40 | should "handle binary" do 41 | assert_equal 7, svc.call.ext.binary(5, 2) 42 | assert_equal 7, svc.call.intTest.binary(5, 2) 43 | end 44 | 45 | should "handle ternary" do 46 | assert_equal 10, svc.call.ext.ternary(5, 2, 3) 47 | assert_equal 10, svc.call.intTest.ternary(5, 2, 3) 48 | end 49 | 50 | should "handle massive binaries" do 51 | assert_equal 8 * 1024 * 1024, svc.call.ext.big(8 * 1024 * 1024).size 52 | assert_equal 8 * 1024 * 1024, svc.call.intTest.big(8 * 1024 * 1024).size 53 | end 54 | 55 | should "not block on internal modules" do 56 | time = Time.now 57 | svc.call.intTest.sleep(1000) 58 | assert(Time.now >= time + 1) 59 | 60 | time = Time.now 61 | svc.cast.intTest.sleep(1000) 62 | svc.cast.intTest.sleep(1000) 63 | svc.cast.intTest.sleep(1000) 64 | svc.call.intTest.zeronary 65 | assert(Time.now < time + 1) 66 | end 67 | 68 | should "get an error on missing module" do 69 | begin 70 | svc.call.failboat.mcfail(:fail) 71 | fail "Expected a BERTRPC::ServerError" 72 | rescue BERTRPC::ServerError => e 73 | assert_equal "No such module 'failboat'", e.message 74 | else 75 | assert false, 'failed to raise on missing module' 76 | end 77 | end 78 | 79 | should "get an error on missing function" do 80 | begin 81 | svc.call.ext.mcfail(:fail) 82 | fail "Expected a BERTRPC::ServerError" 83 | rescue BERTRPC::ServerError => e 84 | assert_equal "No such function 'ext:mcfail'", e.message 85 | else 86 | assert false, 'failed to raise on missing function' 87 | end 88 | 89 | begin 90 | svc.call.intTest.mcfail(:fail) 91 | fail "Expected a BERTRPC::ServerError" 92 | rescue BERTRPC::ServerError => e 93 | assert_equal "No such function 'intTest:mcfail'", e.message 94 | else 95 | assert false, 'failed to raise on missing function' 96 | end 97 | end 98 | end 99 | 100 | context "cast" do 101 | should "be received and return immediately" do 102 | t0 = Time.now 103 | assert_equal nil, svc.cast.ext.set_state(7) 104 | assert Time.now - t0 < 1 105 | assert_equal 7, svc.call.ext.get_state 106 | 107 | t0 = Time.now 108 | assert_equal nil, svc.cast.intTest.set_state(7) 109 | assert Time.now - t0 < 1 110 | sleep 0.25 111 | assert_equal 7, svc.call.intTest.get_state 112 | end 113 | end 114 | 115 | teardown do 116 | shutdown_server 117 | end 118 | end 119 | 120 | context "Two Ernie Servers" do 121 | setup do 122 | start_servers(2) 123 | @servers.each do |svc| 124 | svc.cast.intTest.connect_nodes 125 | end 126 | end 127 | 128 | context "call" do 129 | 130 | should "handle zeronary" do 131 | @servers.each do |svc| 132 | assert_equal :foo, svc.call.ext.zeronary 133 | assert_equal :foo, svc.call.intTest.zeronary 134 | end 135 | end 136 | 137 | should "handle unary" do 138 | @servers.each do |svc| 139 | assert_equal 5, svc.call.ext.unary(5) 140 | assert_equal 5, svc.call.intTest.unary(5) 141 | end 142 | end 143 | 144 | should "handle binary" do 145 | @servers.each do |svc| 146 | assert_equal 7, svc.call.ext.binary(5, 2) 147 | assert_equal 7, svc.call.intTest.binary(5, 2) 148 | end 149 | end 150 | 151 | should "handle ternary" do 152 | @servers.each do |svc| 153 | assert_equal 10, svc.call.ext.ternary(5, 2, 3) 154 | assert_equal 10, svc.call.intTest.ternary(5, 2, 3) 155 | end 156 | end 157 | 158 | should "handle massive binaries" do 159 | @servers.each do |svc| 160 | assert_equal 8 * 1024 * 1024, svc.call.ext.big(8 * 1024 * 1024).size 161 | assert_equal 8 * 1024 * 1024, svc.call.intTest.big(8 * 1024 * 1024).size 162 | end 163 | end 164 | 165 | should "make joined erlang nodes possible" do 166 | assert_equal nil, @servers.first.cast.intTest.set_state(7) 167 | sleep 0.25 168 | assert_equal 7, @servers.last.call.intTest.get_state 169 | end 170 | 171 | end 172 | 173 | teardown do 174 | shutdown_servers(2) 175 | end 176 | end 177 | 178 | protected 179 | 180 | def svc 181 | @servers[rand(@servers.size-1)] 182 | end 183 | 184 | def start_server 185 | start_servers(1) 186 | end 187 | 188 | def shutdown_server 189 | shutdown_servers(1) 190 | end 191 | 192 | def start_servers(n = 1) 193 | n.times do 194 | `#{ERNIE_ROOT}/bin/ernie -c #{ERNIE_ROOT}/test/sample/sample.cfg \ 195 | -P /tmp/ernie#{@servers.size}.pid \ 196 | -p #{PORT + @servers.size} \ 197 | --name ernie#{@servers.size}@127.0.0.1 \ 198 | -d` 199 | 200 | @servers << BERTRPC::Service.new('localhost', PORT + @servers.size) 201 | loop do 202 | begin 203 | @servers.last.call.ext.zeronary 204 | break 205 | rescue Object => e 206 | sleep 0.1 207 | end 208 | end 209 | end 210 | end 211 | 212 | def shutdown_servers(n = nil) 213 | start = @servers.size - 1 214 | last = start - (n || start) 215 | (start).downto(last >= 0 ? last : 0) do |i| 216 | pid = File.read("/tmp/ernie#{i}.pid") 217 | `kill -9 #{pid}` 218 | end 219 | end 220 | 221 | end 222 | -------------------------------------------------------------------------------- /elib/ernie_access_logger.erl: -------------------------------------------------------------------------------- 1 | -module(ernie_access_logger). 2 | -behaviour(gen_server). 3 | 4 | %% api 5 | -export([start_link/1, start/1, acc/1, err/3, reopen/0]). 6 | 7 | %% gen_server callbacks 8 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 9 | terminate/2, code_change/3]). 10 | 11 | -include_lib("ernie.hrl"). 12 | 13 | -record(lstate, {access_file_name = undefined, 14 | access_file = undefined}). 15 | 16 | %%==================================================================== 17 | %% API 18 | %%==================================================================== 19 | 20 | start_link(Args) -> 21 | gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []). 22 | 23 | start(Args) -> 24 | gen_server:start({global, ?MODULE}, ?MODULE, Args, []). 25 | 26 | acc(Request) -> 27 | gen_server:cast(?MODULE, {acc, Request}). 28 | 29 | err(Request, Msg, Args) -> 30 | gen_server:cast(?MODULE, {err, Request, Msg, Args}). 31 | 32 | reopen() -> 33 | gen_server:cast(?MODULE, reopen). 34 | 35 | %%==================================================================== 36 | %% gen_server callbacks 37 | %%==================================================================== 38 | 39 | %%-------------------------------------------------------------------- 40 | %% Function: init(Args) -> {ok, State} | 41 | %% {ok, State, Timeout} | 42 | %% ignore | 43 | %% {stop, Reason} 44 | %% Description: Initiates the server 45 | %%-------------------------------------------------------------------- 46 | init([undefined]) -> 47 | error_logger:info_msg("~p starting~n", [?MODULE]), 48 | {ok, #lstate{}}; 49 | init([AccessFileName]) -> 50 | error_logger:info_msg("~p starting~n", [?MODULE]), 51 | case file:open(AccessFileName, [append]) of 52 | {ok, AccessFile} -> 53 | {ok, _T} = timer:apply_interval(10000, ernie_access_logger, reopen, []), 54 | {ok, #lstate{access_file_name = AccessFileName, 55 | access_file = AccessFile}}; 56 | {error, Error} -> 57 | error_logger:error_msg("Error opening access log ~p: ~p.~n", [AccessFileName, Error]), 58 | {ok, #lstate{}} 59 | end. 60 | 61 | %%-------------------------------------------------------------------- 62 | %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | 63 | %% {reply, Reply, State, Timeout} | 64 | %% {noreply, State} | 65 | %% {noreply, State, Timeout} | 66 | %% {stop, Reason, Reply, State} | 67 | %% {stop, Reason, State} 68 | %% Description: Handling call messages 69 | %%-------------------------------------------------------------------- 70 | handle_call(_Request, _From, State) -> 71 | {reply, ok, State}. 72 | 73 | %%-------------------------------------------------------------------- 74 | %% Function: handle_cast(Msg, State) -> {noreply, State} | 75 | %% {noreply, State, Timeout} | 76 | %% {stop, Reason, State} 77 | %% Description: Handling cast messages 78 | %%-------------------------------------------------------------------- 79 | handle_cast({acc, Request}, State) -> 80 | case State#lstate.access_file_name of 81 | undefined -> ok; 82 | _AccessFilename -> acc(Request, State) 83 | end, 84 | {noreply, State}; 85 | handle_cast({err, Request, Msg, Args}, State) -> 86 | case State#lstate.access_file_name of 87 | undefined -> ok; 88 | _AccessFilename -> err(Request, Msg, Args, State) 89 | end, 90 | {noreply, State}; 91 | handle_cast(reopen, State) -> 92 | case State#lstate.access_file_name of 93 | undefined -> 94 | {noreply, State}; 95 | AccessFileName -> 96 | case file:read_file_info(AccessFileName) of 97 | {ok, _FileInfo} -> 98 | {noreply, State}; 99 | {error, enoent} -> 100 | ok = file:close(State#lstate.access_file), 101 | {ok, AccessFile} = file:open(AccessFileName, [append]), 102 | {noreply, State#lstate{access_file = AccessFile}}; 103 | _OtherError -> 104 | {noreply, #lstate{}} 105 | end 106 | end; 107 | handle_cast(_Msg, State) -> 108 | {noreply, State}. 109 | 110 | handle_info(Msg, State) -> 111 | error_logger:error_msg("Unexpected message: ~p~n", [Msg]), 112 | {noreply, State}. 113 | 114 | terminate(_Reason, _State) -> ok. 115 | code_change(_OldVersion, State, _Extra) -> {ok, State}. 116 | 117 | %%==================================================================== 118 | %% Internal 119 | %%==================================================================== 120 | 121 | acc(Request, State) -> 122 | StatString = stat_string(Request), 123 | ActionString = action_string(Request), 124 | Line = io_lib:fwrite("ACC ~s - ~s~n", [StatString, ActionString]), 125 | file:write(State#lstate.access_file, Line). 126 | 127 | err(Request, Msg, Args, State) -> 128 | StatString = stat_string(Request), 129 | ActionString = action_string(Request), 130 | ErrString = io_lib:fwrite(Msg, Args), 131 | Line = io_lib:fwrite("ERR ~s - ~s : ~s~n", [StatString, ErrString, ActionString]), 132 | file:write(State#lstate.access_file, Line). 133 | 134 | stat_string(Request) -> 135 | Log = Request#request.log, 136 | TAccept = time_tuple_to_iso_8601_date(Log#log.taccept), 137 | D1 = time_difference_in_seconds(Log#log.taccept, Log#log.tprocess), 138 | D2 = time_difference_in_seconds(Log#log.tprocess, Log#log.tdone), 139 | Type = Log#log.type, 140 | HQ = Log#log.hq, 141 | LQ = Log#log.lq, 142 | Prio = Request#request.priority, 143 | Args = [TAccept, D1, D2, HQ, LQ, Type, Prio], 144 | io_lib:fwrite("[~s] ~f ~f - ~B ~B ~3s ~p", Args). 145 | 146 | action_string(Request) -> 147 | TermAction = binary_to_term(Request#request.action), 148 | RawAction = lists:flatten(io_lib:fwrite("~1000000000.0.0p", [TermAction])), 149 | case string:len(RawAction) > 150 of 150 | true -> 151 | Action = re:replace(RawAction, "\n", "", [global, {return, list}]), 152 | [string:sub_string(Action, 1, 150), "..."]; 153 | false -> 154 | RawAction 155 | end. 156 | 157 | time_tuple_to_iso_8601_date(TimeTuple) -> 158 | {{YY, MM, DD}, {H, M, S}} = calendar:now_to_local_time(TimeTuple), 159 | {_MegaSecs, _Secs, MicroSecs} = TimeTuple, 160 | Args = [YY, MM, DD, H, M, S, MicroSecs], 161 | io_lib:fwrite("~4B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B.~-6.10.0B", Args). 162 | 163 | time_difference_in_seconds(T1, T2) -> 164 | {_, _, MS1} = T1, 165 | {_, _, MS2} = T2, 166 | S1 = calendar:datetime_to_gregorian_seconds(calendar:now_to_local_time(T1)), 167 | S2 = calendar:datetime_to_gregorian_seconds(calendar:now_to_local_time(T2)), 168 | F1 = S1 + (MS1 / 1000000), 169 | F2 = S2 + (MS2 / 1000000), 170 | F2 - F1. -------------------------------------------------------------------------------- /lib/ernie.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bert' 3 | require 'logger' 4 | 5 | class Ernie 6 | VERSION = '2.5.2' 7 | 8 | class << self 9 | attr_accessor :mods, :current_mod, :log 10 | attr_accessor :auto_start 11 | attr_accessor :count, :virgin_procline 12 | end 13 | 14 | self.count = 0 15 | self.virgin_procline = $0 16 | self.mods = {} 17 | self.current_mod = nil 18 | self.log = Logger.new(STDOUT) 19 | self.log.level = Logger::FATAL 20 | self.auto_start = true 21 | 22 | # Record a module. 23 | # +name+ is the module Symbol 24 | # +block+ is the Block containing function definitions 25 | # 26 | # Returns nothing 27 | def self.mod(name, block) 28 | m = Mod.new(name) 29 | self.current_mod = m 30 | self.mods[name] = m 31 | block.call 32 | end 33 | 34 | # Record a function. 35 | # +name+ is the function Symbol 36 | # +block+ is the Block to associate 37 | # 38 | # Returns nothing 39 | def self.fun(name, block) 40 | self.current_mod.fun(name, block) 41 | end 42 | 43 | # Expose all public methods in a Ruby module: 44 | # +name+ is the ernie module Symbol 45 | # +mixin+ is the ruby module whose public methods are exposed 46 | # 47 | # Returns nothing 48 | def self.expose(name, mixin) 49 | context = Object.new 50 | context.extend mixin 51 | mod(name, lambda { 52 | mixin.public_instance_methods.each do |meth| 53 | fun(meth.to_sym, context.method(meth)) 54 | end 55 | }) 56 | context 57 | end 58 | 59 | # Set the logfile to given path. 60 | # +file+ is the String path to the logfile 61 | # 62 | # Returns nothing 63 | def self.logfile(file) 64 | self.log = Logger.new(file) 65 | end 66 | 67 | # Set the log level. 68 | # +level+ is the Logger level (Logger::WARN, etc) 69 | # 70 | # Returns nothing 71 | def self.loglevel(level) 72 | self.log.level = level 73 | end 74 | 75 | # Dispatch the request to the proper mod:fun. 76 | # +mod+ is the module Symbol 77 | # +fun+ is the function Symbol 78 | # +args+ is the Array of arguments 79 | # 80 | # Returns the Ruby object response 81 | def self.dispatch(mod, fun, args) 82 | self.mods[mod] || raise(ServerError.new("No such module '#{mod}'")) 83 | self.mods[mod].funs[fun] || raise(ServerError.new("No such function '#{mod}:#{fun}'")) 84 | self.mods[mod].funs[fun].call(*args) 85 | end 86 | 87 | # Read the length header from the wire. 88 | # +input+ is the IO from which to read 89 | # 90 | # Returns the size Integer if one was read 91 | # Returns nil otherwise 92 | def self.read_4(input) 93 | raw = input.read(4) 94 | return nil unless raw 95 | raw.unpack('N').first 96 | end 97 | 98 | # Read a BERP from the wire and decode it to a Ruby object. 99 | # +input+ is the IO from which to read 100 | # 101 | # Returns a Ruby object if one could be read 102 | # Returns nil otherwise 103 | def self.read_berp(input) 104 | packet_size = self.read_4(input) 105 | return nil unless packet_size 106 | bert = input.read(packet_size) 107 | BERT.decode(bert) 108 | end 109 | 110 | # Write the given Ruby object to the wire as a BERP. 111 | # +output+ is the IO on which to write 112 | # +ruby+ is the Ruby object to encode 113 | # 114 | # Returns nothing 115 | def self.write_berp(output, ruby) 116 | data = BERT.encode(ruby) 117 | output.write([data.length].pack("N")) 118 | output.write(data) 119 | end 120 | 121 | # Start the processing loop. 122 | # 123 | # Loops forever 124 | def self.start 125 | self.procline('starting') 126 | self.log.info("(#{Process.pid}) Starting") if self.log.level <= Logger::INFO 127 | self.log.debug(self.mods.inspect) if self.log.level <= Logger::DEBUG 128 | 129 | input = IO.new(3) 130 | output = IO.new(4) 131 | input.sync = true 132 | output.sync = true 133 | 134 | loop do 135 | process(input, output) 136 | end 137 | end 138 | 139 | # Processes a single BertRPC command. 140 | # 141 | # input - The IO to #read command BERP from. 142 | # output - The IO to #write reply BERP to. 143 | # 144 | # Returns a [iruby, oruby] tuple of incoming and outgoing BERPs processed. 145 | def self.process(input, output) 146 | self.procline('waiting') 147 | iruby = self.read_berp(input) 148 | self.count += 1 149 | 150 | unless iruby 151 | puts "Could not read BERP length header. Ernie server may have gone away. Exiting now." 152 | if self.log.level <= Logger::INFO 153 | self.log.info("(#{Process.pid}) Could not read BERP length header. Ernie server may have gone away. Exiting now.") 154 | end 155 | exit! 156 | end 157 | 158 | if iruby.size == 4 && iruby[0] == :call 159 | mod, fun, args = iruby[1..3] 160 | self.procline("#{mod}:#{fun}(#{args})") 161 | self.log.info("-> " + iruby.inspect) if self.log.level <= Logger::INFO 162 | begin 163 | res = self.dispatch(mod, fun, args) 164 | oruby = t[:reply, res] 165 | self.log.debug("<- " + oruby.inspect) if self.log.level <= Logger::DEBUG 166 | write_berp(output, oruby) 167 | rescue ServerError => e 168 | oruby = t[:error, t[:server, 0, e.class.to_s, e.message, e.backtrace]] 169 | self.log.error("<- " + oruby.inspect) if self.log.level <= Logger::ERROR 170 | self.log.error(e.backtrace.join("\n")) if self.log.level <= Logger::ERROR 171 | write_berp(output, oruby) 172 | rescue Object => e 173 | oruby = t[:error, t[:user, 0, e.class.to_s, e.message, e.backtrace]] 174 | self.log.error("<- " + oruby.inspect) if self.log.level <= Logger::ERROR 175 | self.log.error(e.backtrace.join("\n")) if self.log.level <= Logger::ERROR 176 | write_berp(output, oruby) 177 | end 178 | elsif iruby.size == 4 && iruby[0] == :cast 179 | mod, fun, args = iruby[1..3] 180 | self.procline("#{mod}:#{fun}(#{args})") 181 | self.log.info("-> " + [:cast, mod, fun, args].inspect) if self.log.level <= Logger::INFO 182 | begin 183 | self.dispatch(mod, fun, args) 184 | rescue Object => e 185 | # ignore 186 | end 187 | oruby = t[:noreply] 188 | write_berp(output, oruby) 189 | else 190 | self.procline("invalid request") 191 | self.log.error("-> " + iruby.inspect) if self.log.level <= Logger::ERROR 192 | oruby = t[:error, t[:server, 0, "Invalid request: #{iruby.inspect}"]] 193 | self.log.error("<- " + oruby.inspect) if self.log.level <= Logger::ERROR 194 | write_berp(output, oruby) 195 | end 196 | 197 | [iruby, oruby] 198 | end 199 | 200 | def self.procline(msg) 201 | $0 = "ernie handler #{VERSION} (ruby) - #{self.virgin_procline} - [#{self.count}] #{msg}"[0..159] 202 | end 203 | 204 | def self.version 205 | VERSION 206 | end 207 | end 208 | 209 | class Ernie::ServerError < StandardError; end 210 | 211 | class Ernie::Mod 212 | attr_accessor :name, :funs 213 | 214 | def initialize(name) 215 | self.name = name 216 | self.funs = {} 217 | end 218 | 219 | def fun(name, block) 220 | raise TypeError, "block required" if block.nil? 221 | self.funs[name] = block 222 | end 223 | end 224 | 225 | # Root level calls 226 | 227 | def logfile(name) 228 | Ernie.logfile(name) 229 | end 230 | 231 | def loglevel(level) 232 | Ernie.loglevel(level) 233 | end 234 | 235 | at_exit do 236 | Ernie.start if Ernie.auto_start 237 | end 238 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ernie 2 | ===== 3 | 4 | By Tom Preston-Werner (tom@mojombo.com) 5 | 6 | Ernie is a BERT-RPC server implementation that uses an Erlang server to accept 7 | incoming connections, and then delegates the request to custom modules that 8 | you can write in any language (currently only Ruby and Erlang support is 9 | included). 10 | 11 | Modules that are written in Ruby or any non-Erlang language are known as 12 | "external" modules and you must specify how many workers of each module should 13 | be spawned. Requests against these modules are balanced between the workers. 14 | Modules that are written in Erlang are known as "native" modules and run 15 | within the Erlang server's runtime. Since these are spawned as lightweight 16 | processes, there is no balancing necessary and much less communication 17 | overhead when compared to external modules. 18 | 19 | Ernie supports multiple heterogenous modules. For instance, you can have an 20 | external Ruby module running 10 workers *and* a native Erlang module running 21 | simultaneously. Ernie keeps track of sending requests to the proper module. 22 | Using a technique called "shadowing," you can selectively optimize certain 23 | external module functions with native code and Ernie will handle selecting the 24 | correct function. 25 | 26 | See the full BERT-RPC specification at [bert-rpc.org](http://bert-rpc.org). 27 | 28 | Ernie currently supports the following BERT-RPC features: 29 | 30 | * `call` requests 31 | * `cast` requests 32 | 33 | Ernie was developed for GitHub and is currently in production use serving 34 | millions of RPC requests every day. The stability and performance have been 35 | exemplary. 36 | 37 | Ernie follows [Semantic Versioning](http://semver.org/) for release 38 | versioning. 39 | 40 | Installation 41 | ------------ 42 | 43 | Step 1: Install Erlang (R13B or higher). 44 | 45 | http://www.erlang.org/download.html 46 | 47 | Step 2: Install Ernie: 48 | 49 | $ [sudo] gem install ernie 50 | 51 | 52 | Running 53 | ------- 54 | 55 | Usage: ernie [command] [options] 56 | -c, --config CONFIG Config file. 57 | -p, --port PORT Port. 58 | -l, --log-level Log level (0-4). 59 | -a, --access-log LOGFILE Access log file. 60 | -d, --detached Run as a daemon. 61 | -P, --pidfile PIDFILE Location to write pid file. 62 | --name NAME Erlang process name. 63 | --sname SNAME Erlang short process name. 64 | -E, --erlang ERLANG_OPTIONS Options passed to Erlang VM. 65 | 66 | Commands: 67 | Start an Ernie server. 68 | reload-handlers Gracefully reload all of the external handlers 69 | and use the new code for all subsequent requests. 70 | stats Print a list of connection and handler statistics. 71 | 72 | Examples: 73 | ernie -d -p 9999 -c example.cfg 74 | Start the ernie server in the background on port 9999 using the 75 | example.cfg configuration file. 76 | 77 | ernie reload-handlers -p 9999 78 | Reload the handlers for the ernie server currently running on 79 | port 9999. 80 | 81 | ernie -c example.cfg -E '-run mymodule' 82 | Start the ernie server with an additional erlang module called 83 | 'mymodule' 84 | 85 | 86 | Configuration File 87 | ------------------ 88 | 89 | Ernie configuration files are written as a series of dotted Erlang terms. Each 90 | term is a list of 2-tuples that specify options for a module. 91 | 92 | ### Native Modules 93 | 94 | The form for native modules is: 95 | 96 | [{module, Module}, 97 | {type, native}, 98 | {codepaths, CodePaths}]. 99 | 100 | Where Module is an atom corresponding to the module name and CodePaths is a 101 | list of strings representing the file paths that should be added to the 102 | runtime's code path. These paths will be prepended to the code path and must 103 | include the native module's directory and the directories of any dependencies. 104 | 105 | ### External Modules 106 | 107 | The form for external modules is: 108 | 109 | [{module, Module}, 110 | {type, external}, 111 | {command, Command}, 112 | {count, Count}]. 113 | 114 | Where Module is an atom corresponding to the module name, Command is a string 115 | specifying the command to be executed in order to start a worker, and Count is 116 | the number of workers to spawn. 117 | 118 | ### Shadowing 119 | 120 | If you specify a native module and an external module of the same name (and in 121 | that order), Ernie will inspect the native module to see if it has the 122 | requested function exported and use that if it does. If it does not, then it 123 | will fall back on the external module. This can be used to selectively 124 | optimize certain functions in a module without any modifications to your 125 | client code. 126 | 127 | ### Predicate Shadowing 128 | 129 | In some circumstances it can be nice to conditionally shadow a function in an 130 | external module based on the nature of the arguments. For example, you might 131 | want requests for `math:fib(X)` to be routed to the external module when X is 132 | less than 10, but to be handled by the native module when X is 10 or greater. 133 | This can be accomplished by implementing a function `math:fib_pred(X)` in the 134 | native module. Notice the `_pred` appended to the normal function name (pred 135 | is short for predicate). If a function like this is present, Ernie will call 136 | it with the requested arguments and if the return value is `true` the native 137 | module will be used. If the return value is `false` the external module will 138 | be used. 139 | 140 | 141 | Example Configuration File 142 | -------------------------- 143 | 144 | The following example config file informs Ernie of two modules. The first term 145 | identifies a native module 'nat' that resides in the nat.beam file under the 146 | '/path/to/app/ebin' directory. The second term specifies an external module 147 | 'ext' that will have 2 workers started with the command 'ruby 148 | /path/to/app/ernie/ext.rb'. 149 | 150 | [{module, nat}, 151 | {type, native}, 152 | {codepaths, ["/path/to/app/ebin"]}]. 153 | 154 | [{module, ext}, 155 | {type, external}, 156 | {command, "ruby /path/to/app/ernie/ext.rb"}, 157 | {count, 2}]. 158 | 159 | 160 | Access Log 161 | ---------- 162 | 163 | If you have requested that an access log be written (using the -a or 164 | --access-log option) then all requests will be logged to that file. Each 165 | request is printed on a single line. The elements of the log line are as 166 | follows (with comments on the right side): 167 | 168 | ACC type of message [ ACC | ERR ] 169 | [2010-02-20T11:42:25.259750] time the connection was accepted 170 | 0.000053 seconds from connection to processing start 171 | 0.000237 seconds from processing start to finish 172 | - delimiter 173 | 0 size of high queue at connect time 174 | 0 size of low queue at connect time 175 | nat type of handler [ nat | ext ] 176 | high priority [ high | low ] 177 | - delimiter 178 | {call,nat,add,[1,2]} message 179 | 180 | 181 | Log lines are written when the request completes so they may appear out of 182 | order with respect to connection time. To facilitate log rotation, Ernie will 183 | create a new access log file if the current log file is moved or deleted. 184 | 185 | 186 | Native (Erlang) Handler 187 | ----------------------- 188 | 189 | Native handlers are written as normal Erlang modules. The exported functions 190 | will become available to BERT-RPC clients. 191 | 192 | ### Example 193 | 194 | -module(nat). 195 | -export([add/2]). 196 | 197 | add(A, B) -> 198 | A + B. 199 | 200 | ### BERT-RPC Sequence Example 201 | 202 | -> {call, nat, add, [1, 2]} 203 | <- {reply, 3} 204 | 205 | 206 | External (Ruby) Handler 207 | ----------------------- 208 | 209 | Included in this gem is a library called `ernie` that makes it easy to write 210 | Ernie handlers in Ruby. All you have to do is write a standard Ruby module and 211 | expose it to Ernie and the functions of that module will become available to 212 | BERT-RPC clients. 213 | 214 | ### Example 215 | 216 | Using a Ruby module and Ernie.expose: 217 | 218 | require 'rubygems' 219 | require 'ernie' 220 | 221 | module Ext 222 | def add(a, b) 223 | a + b 224 | end 225 | end 226 | 227 | Ernie.expose(:ext, Ext) 228 | 229 | ### BERT-RPC Sequence Example 230 | 231 | -> {call, nat, add, [1, 2]} 232 | <- {reply, 3} 233 | 234 | ### Logging 235 | 236 | You can have logging sent to a file by adding these lines to your handler: 237 | 238 | logfile('/var/log/ernie.log') 239 | loglevel(Logger::INFO) 240 | 241 | This will log startup info, requests, and error messages to the log. Choosing 242 | Logger::DEBUG will include the response (be careful, doing this can generate 243 | very large log files). 244 | 245 | ### Autostart 246 | 247 | Normally Ruby Ernie handlers will become active after the file has been loaded 248 | in. you can disable this behavior by setting: 249 | 250 | Ernie.auto_start = false 251 | 252 | 253 | Selecting Queue Priority 254 | ------------------------ 255 | 256 | Ernie maintains High and Low priority queues for incoming connections. If 257 | there are any connections in the High priority queue, they will always be 258 | processed first. If the High priority queue is empty, connections will be 259 | processed from the Low priority queue. By default, connections go into the 260 | High priority queue. To select a queue, an info BERP of the following form 261 | must be sent preceding the call. 262 | 263 | -- {info, priority, Priority} 264 | 265 | Where `Priority` is either the `high` or `low` atom. An example sequence where 266 | the low priority queue is being selected would look like the following. 267 | 268 | -> {info, priority, low} 269 | -> {call, nat, add, [1, 2]} 270 | <- {reply, 3} 271 | 272 | 273 | Using the BERTRPC gem to make calls to Ernie 274 | -------------------------------------------- 275 | 276 | You can make BERT-RPC calls from Ruby with the [BERTRPC gem](http://github.com/mojombo/bertrpc): 277 | 278 | require 'bertrpc' 279 | 280 | svc = BERTRPC::Service.new('localhost', 8000) 281 | svc.call.ext.add(1, 2) 282 | # => 3 283 | 284 | 285 | Contribute 286 | ---------- 287 | 288 | If you'd like to hack on Ernie, start by forking my repo on GitHub: 289 | 290 | http://github.com/mojombo/ernie 291 | 292 | To get all of the dependencies, install the gem first. To run ernie from 293 | source, you must first build the Erlang code: 294 | 295 | rake ebuild 296 | 297 | The best way to get your changes merged back into core is as follows: 298 | 299 | 1. Clone down your fork 300 | 1. Create a topic branch to contain your change 301 | 1. Hack away 302 | 1. Add tests and make sure everything still passes by running `rake` 303 | 1. If you are adding new functionality, document it in the README.md 304 | 1. Do not change the version number, I will do that on my end 305 | 1. If necessary, rebase your commits into logical chunks, without errors 306 | 1. Push the branch up to GitHub 307 | 1. Send me (mojombo) a pull request for your branch 308 | 309 | 310 | Copyright 311 | --------- 312 | 313 | Copyright (c) 2009 Tom Preston-Werner. See LICENSE for details. 314 | -------------------------------------------------------------------------------- /elib/ernie_server.erl: -------------------------------------------------------------------------------- 1 | -module(ernie_server). 2 | -behaviour(gen_server). 3 | -include_lib("ernie.hrl"). 4 | 5 | %% api 6 | -export([start_link/1, start/1, process/1, enqueue_request/1, kick/0, fin/0]). 7 | 8 | %% gen_server callbacks 9 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 10 | terminate/2, code_change/3]). 11 | 12 | %%==================================================================== 13 | %% API 14 | %%==================================================================== 15 | 16 | start_link(Args) -> 17 | gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []). 18 | 19 | start(Args) -> 20 | gen_server:start({local, ?MODULE}, ?MODULE, Args, []). 21 | 22 | process(Sock) -> 23 | gen_server:cast(?MODULE, {process, Sock}). 24 | 25 | enqueue_request(Request) -> 26 | gen_server:call(?MODULE, {enqueue_request, Request}). 27 | 28 | kick() -> 29 | gen_server:cast(?MODULE, kick). 30 | 31 | fin() -> 32 | gen_server:cast(?MODULE, fin). 33 | 34 | %%==================================================================== 35 | %% gen_server callbacks 36 | %%==================================================================== 37 | 38 | %%-------------------------------------------------------------------- 39 | %% Function: init(Args) -> {ok, State} | 40 | %% {ok, State, Timeout} | 41 | %% ignore | 42 | %% {stop, Reason} 43 | %% Description: Initiates the server 44 | %%-------------------------------------------------------------------- 45 | init([Port, Configs]) -> 46 | process_flag(trap_exit, true), 47 | error_logger:info_msg("~p starting~n", [?MODULE]), 48 | {ok, LSock} = try_listen(Port, 500), 49 | spawn(fun() -> loop(LSock) end), 50 | Map = init_map(Configs), 51 | io:format("pidmap = ~p~n", [Map]), 52 | {ok, #state{lsock = LSock, map = Map}}. 53 | 54 | %%-------------------------------------------------------------------- 55 | %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | 56 | %% {reply, Reply, State, Timeout} | 57 | %% {noreply, State} | 58 | %% {noreply, State, Timeout} | 59 | %% {stop, Reason, Reply, State} | 60 | %% {stop, Reason, State} 61 | %% Description: Handling call messages 62 | %%-------------------------------------------------------------------- 63 | handle_call({enqueue_request, Request}, _From, State) -> 64 | case Request#request.priority of 65 | high -> 66 | Hq2 = queue:in(Request, State#state.hq), 67 | Lq2 = State#state.lq; 68 | low -> 69 | Hq2 = State#state.hq, 70 | Lq2 = queue:in(Request, State#state.lq) 71 | end, 72 | {reply, ok, State#state{hq = Hq2, lq = Lq2}}; 73 | handle_call(_Request, _From, State) -> 74 | {reply, ok, State}. 75 | 76 | %%-------------------------------------------------------------------- 77 | %% Function: handle_cast(Msg, State) -> {noreply, State} | 78 | %% {noreply, State, Timeout} | 79 | %% {stop, Reason, State} 80 | %% Description: Handling cast messages 81 | %%-------------------------------------------------------------------- 82 | handle_cast({process, Sock}, State) -> 83 | Log = #log{hq = queue:len(State#state.hq), 84 | lq = queue:len(State#state.lq), 85 | taccept = erlang:now()}, 86 | Request = #request{sock = Sock, log = Log}, 87 | spawn(fun() -> receive_term(Request, State) end), 88 | logger:debug("Spawned receiver~n", []), 89 | {noreply, State}; 90 | handle_cast(kick, State) -> 91 | case queue:out(State#state.hq) of 92 | {{value, Request}, Hq2} -> 93 | State2 = process_request(Request, hq, Hq2, State), 94 | {noreply, State2}; 95 | {empty, _Hq} -> 96 | case queue:out(State#state.lq) of 97 | {{value, Request}, Lq2} -> 98 | State2 = process_request(Request, lq, Lq2, State), 99 | {noreply, State2}; 100 | {empty, _Lq} -> 101 | {noreply, State} 102 | end 103 | end; 104 | handle_cast(fin, State) -> 105 | Listen = State#state.listen, 106 | Count = State#state.count, 107 | ZCount = State#state.zcount + 1, 108 | logger:debug("Fin; Listen = ~p (~p/~p)~n", [Listen, Count, ZCount]), 109 | case Listen =:= false andalso ZCount =:= Count of 110 | true -> halt(); 111 | false -> {noreply, State#state{zcount = ZCount}} 112 | end; 113 | handle_cast(_Msg, State) -> {noreply, State}. 114 | 115 | handle_info(Msg, State) -> 116 | error_logger:error_msg("Unexpected message: ~p~n", [Msg]), 117 | {noreply, State}. 118 | 119 | terminate(_Reason, _State) -> ok. 120 | code_change(_OldVersion, State, _Extra) -> {ok, State}. 121 | 122 | %%==================================================================== 123 | %% Internal 124 | %%==================================================================== 125 | 126 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 127 | % Module mapping 128 | 129 | init_map(Configs) -> 130 | lists:map((fun extract_mapping/1), Configs). 131 | 132 | extract_mapping(Config) -> 133 | Id = proplists:get_value(id, Config), 134 | Mod = proplists:get_value(module, Config), 135 | {Mod, Id}. 136 | 137 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 138 | % Listen and loop 139 | 140 | try_listen(Port, 0) -> 141 | error_logger:error_msg("Could not listen on port ~p~n", [Port]), 142 | {error, "Could not listen on port"}; 143 | try_listen(Port, Times) -> 144 | Res = gen_tcp:listen(Port, [binary, {packet, 4}, {active, false}, {reuseaddr, true}, {backlog, 128}]), 145 | case Res of 146 | {ok, LSock} -> 147 | error_logger:info_msg("Listening on port ~p~n", [Port]), 148 | % gen_tcp:controlling_process(LSock, ernie_server), 149 | {ok, LSock}; 150 | {error, Reason} -> 151 | error_logger:info_msg("Could not listen on port ~p: ~p~n", [Port, Reason]), 152 | timer:sleep(5000), 153 | try_listen(Port, Times - 1) 154 | end. 155 | 156 | loop(LSock) -> 157 | case gen_tcp:accept(LSock) of 158 | {error, closed} -> 159 | logger:debug("Listen socket closed~n", []), 160 | timer:sleep(infinity); 161 | {error, Error} -> 162 | logger:debug("Connection accept error: ~p~n", [Error]), 163 | loop(LSock); 164 | {ok, Sock} -> 165 | logger:debug("Accepted socket: ~p~n", [Sock]), 166 | ernie_server:process(Sock), 167 | loop(LSock) 168 | end. 169 | 170 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 171 | % Receive and process 172 | 173 | receive_term(Request, State) -> 174 | Sock = Request#request.sock, 175 | case gen_tcp:recv(Sock, 0) of 176 | {ok, BinaryTerm} -> 177 | logger:debug("Got binary term: ~p~n", [BinaryTerm]), 178 | Term = binary_to_term(BinaryTerm), 179 | logger:info("Got term: ~p~n", [Term]), 180 | case Term of 181 | {call, '__admin__', Fun, Args} -> 182 | ernie_admin:process(Sock, Fun, Args, State); 183 | {info, Command, Args} -> 184 | Infos = Request#request.infos, 185 | Infos2 = [BinaryTerm | Infos], 186 | Request2 = Request#request{infos = Infos2}, 187 | Request3 = process_info(Request2, Command, Args), 188 | receive_term(Request3, State); 189 | _Any -> 190 | Request2 = Request#request{action = BinaryTerm}, 191 | close_if_cast(Term, Request2), 192 | ernie_server:enqueue_request(Request2), 193 | ernie_server:kick() 194 | end; 195 | {error, closed} -> 196 | ok = gen_tcp:close(Sock) 197 | end. 198 | 199 | process_info(Request, priority, [Priority]) -> 200 | Request#request{priority = Priority}; 201 | process_info(Request, _Command, _Args) -> 202 | Request. 203 | 204 | process_request(Request, Priority, Q2, State) -> 205 | ActionTerm = bert:decode(Request#request.action), 206 | {_Type, Mod, _Fun, _Args} = ActionTerm, 207 | Specs = lists:filter(fun({X, _Id}) -> Mod =:= X end, State#state.map), 208 | case Specs of 209 | [] -> no_module(Mod, Request, Priority, Q2, State); 210 | _Else -> process_module(ActionTerm, Specs, Request, Priority, Q2, State) 211 | end. 212 | 213 | no_module(Mod, Request, Priority, Q2, State) -> 214 | logger:debug("No such module ~p~n", [Mod]), 215 | Sock = Request#request.sock, 216 | Class = <<"ServerError">>, 217 | Message = list_to_binary(io_lib:format("No such module '~p'", [Mod])), 218 | gen_tcp:send(Sock, term_to_binary({error, [server, 0, Class, Message, []]})), 219 | ok = gen_tcp:close(Sock), 220 | finish(Priority, Q2, State). 221 | 222 | process_module(ActionTerm, [], Request, Priority, Q2, State) -> 223 | {_Type, Mod, Fun, _Args} = ActionTerm, 224 | logger:debug("No such function ~p:~p~n", [Mod, Fun]), 225 | Sock = Request#request.sock, 226 | Class = <<"ServerError">>, 227 | Message = list_to_binary(io_lib:format("No such function '~p:~p'", [Mod, Fun])), 228 | gen_tcp:send(Sock, term_to_binary({error, [server, 0, Class, Message, []]})), 229 | ok = gen_tcp:close(Sock), 230 | finish(Priority, Q2, State); 231 | process_module(ActionTerm, Specs, Request, Priority, Q2, State) -> 232 | [{_Mod, Id} | OtherSpecs] = Specs, 233 | case Id of 234 | native -> 235 | logger:debug("Dispatching to native module~n", []), 236 | {_Type, Mod, Fun, Args} = ActionTerm, 237 | case erlang:function_exported(Mod, Fun, length(Args)) of 238 | false -> 239 | logger:debug("Not found in native module ~p~n", [Mod]), 240 | process_module(ActionTerm, OtherSpecs, Request, Priority, Q2, State); 241 | true -> 242 | PredFun = list_to_atom(atom_to_list(Fun) ++ "_pred"), 243 | logger:debug("Checking ~p:~p(~p) for selection.~n", [Mod, PredFun, Args]), 244 | case erlang:function_exported(Mod, PredFun, length(Args)) of 245 | false -> 246 | logger:debug("No such predicate function ~p:~p(~p).~n", [Mod, PredFun, Args]), 247 | process_native_request(ActionTerm, Request, Priority, Q2, State); 248 | true -> 249 | case apply(Mod, PredFun, Args) of 250 | false -> 251 | logger:debug("Predicate ~p:~p(~p) returned false.~n", [Mod, PredFun, Args]), 252 | process_module(ActionTerm, OtherSpecs, Request, Priority, Q2, State); 253 | true -> 254 | logger:debug("Predicate ~p:~p(~p) returned true.~n", [Mod, PredFun, Args]), 255 | process_native_request(ActionTerm, Request, Priority, Q2, State) 256 | end 257 | end 258 | end; 259 | ValidPid when is_pid(ValidPid) -> 260 | logger:debug("Found external pid ~p~n", [ValidPid]), 261 | process_external_request(ValidPid, Request, Priority, Q2, State) 262 | end. 263 | 264 | close_if_cast(ActionTerm, Request) -> 265 | case ActionTerm of 266 | {cast, _Mod, _Fun, _Args} -> 267 | Sock = Request#request.sock, 268 | gen_tcp:send(Sock, term_to_binary({noreply})), 269 | ok = gen_tcp:close(Sock), 270 | logger:debug("Closed cast.~n", []); 271 | _Any -> 272 | ok 273 | end. 274 | 275 | finish(Priority, Q2, State) -> 276 | case Priority of 277 | hq -> State#state{hq = Q2}; 278 | lq -> State#state{lq = Q2} 279 | end. 280 | 281 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 282 | % Native 283 | 284 | process_native_request(ActionTerm, Request, Priority, Q2, State) -> 285 | Count = State#state.count, 286 | State2 = State#state{count = Count + 1}, 287 | logger:debug("Count = ~p~n", [Count + 1]), 288 | Log = Request#request.log, 289 | Log2 = Log#log{type = native, tprocess = erlang:now()}, 290 | Request2 = Request#request{log = Log2}, 291 | spawn(fun() -> ernie_native:process(ActionTerm, Request2) end), 292 | finish(Priority, Q2, State2). 293 | 294 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 295 | % External 296 | 297 | process_external_request(Pid, Request, Priority, Q2, State) -> 298 | Count = State#state.count, 299 | State2 = State#state{count = Count + 1}, 300 | logger:debug("Count = ~p~n", [Count + 1]), 301 | case asset_pool:lease(Pid) of 302 | {ok, Asset} -> 303 | logger:debug("Leased asset for pool ~p~n", [Pid]), 304 | Log = Request#request.log, 305 | Log2 = Log#log{type = external, tprocess = erlang:now()}, 306 | Request2 = Request#request{log = Log2}, 307 | spawn(fun() -> process_now(Pid, Request2, Asset) end), 308 | finish(Priority, Q2, State2); 309 | empty -> 310 | State 311 | end. 312 | 313 | process_now(Pid, Request, Asset) -> 314 | try unsafe_process_now(Request, Asset) of 315 | _AnyResponse -> 316 | Log = Request#request.log, 317 | Log2 = Log#log{tdone = erlang:now()}, 318 | Request2 = Request#request{log = Log2}, 319 | ernie_access_logger:acc(Request2) 320 | catch 321 | AnyClass:AnyError -> 322 | Log = Request#request.log, 323 | Log2 = Log#log{tdone = erlang:now()}, 324 | Request2 = Request#request{log = Log2}, 325 | ernie_access_logger:err(Request2, "External process error ~w: ~w", [AnyClass, AnyError]) 326 | after 327 | asset_pool:return(Pid, Asset), 328 | ernie_server:fin(), 329 | ernie_server:kick(), 330 | logger:debug("Returned asset ~p~n", [Asset]), 331 | gen_tcp:close(Request#request.sock), 332 | logger:debug("Closed socket ~p~n", [Request#request.sock]) 333 | end. 334 | 335 | unsafe_process_now(Request, Asset) -> 336 | BinaryTerm = Request#request.action, 337 | Term = binary_to_term(BinaryTerm), 338 | case Term of 339 | {call, Mod, Fun, Args} -> 340 | logger:debug("Calling ~p:~p(~p)~n", [Mod, Fun, Args]), 341 | Sock = Request#request.sock, 342 | {asset, Port, Token} = Asset, 343 | logger:debug("Asset: ~p ~p~n", [Port, Token]), 344 | {ok, Data} = port_wrapper:rpc(Port, BinaryTerm), 345 | ok = gen_tcp:send(Sock, Data); 346 | {cast, Mod, Fun, Args} -> 347 | logger:debug("Casting ~p:~p(~p)~n", [Mod, Fun, Args]), 348 | {asset, Port, Token} = Asset, 349 | logger:debug("Asset: ~p ~p~n", [Port, Token]), 350 | {ok, _Data} = port_wrapper:rpc(Port, BinaryTerm) 351 | end. --------------------------------------------------------------------------------