ernie 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.md
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *.beam
2
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
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.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ Ernie
2
+ =====
3
+
4
+ By Tom Preston-Werner (tom@mojombo.com)
5
+
6
+ WARNING: This software is alpha and should not be used in production without
7
+ extensive testing. You should not consider this project production ready until
8
+ it is released as 1.0.
9
+
10
+
11
+ Description
12
+ -----------
13
+
14
+ Ernie is a BERT-RPC server implementation that uses an Erlang server to accept incoming connections, and then delegates the request to a Ruby handler via Erlectricity.
15
+
16
+
17
+ Installation
18
+ ------------
19
+
20
+ You must have Erlang installed before installing Ernie.
21
+
22
+ gem install mojombo-ernie -s http://gems.github.com
23
+
24
+
25
+ Running
26
+ -------
27
+
28
+ Usage: ernie [command] [options]
29
+ -h, --handler HANDLER Handler file
30
+ -p, --port PORT Port
31
+ -n, --number NUMBER Number of handler instances
32
+ -d, --detached Run as a daemon
33
+ -P, --pidfile PIDFILE Location to write pid file.
34
+
35
+ Commands:
36
+ <none> Start an Ernie server.
37
+ reload-handlers Gracefully reload all of the the ruby handlers
38
+ and use the new code for all subsequent requests.
39
+
40
+ Examples:
41
+ ernie -d -p 9999 -n 10 -h calc.rb
42
+ Start the ernie server in the background on port 9999 with ten
43
+ handlers, using the calc.rb handler file.
44
+
45
+ ernie reload-handlers -p 9999
46
+ Reload the handlers for the ernie server currently running on
47
+ port 9999.
48
+
49
+ Example Handler
50
+ ---------------
51
+
52
+ require 'ernie'
53
+
54
+ mod(:calc) do
55
+ fun(:add) do |a, b|
56
+ a + b
57
+ end
58
+ end
59
+
60
+
61
+ Example BERT-RPC call for above example
62
+ ---------------------------------------
63
+
64
+ -> {call, calc, add, [1, 2]}
65
+
66
+ <- {reply, 3}
67
+
68
+
69
+ Using the BERTRPC gem to make calls to Ernie
70
+ --------------------------------------------
71
+
72
+ You can make BERT-RPC calls from Ruby with the [BERTRPC gem](http://github.com/mojombo/bertrpc):
73
+
74
+ require 'bertrpc'
75
+
76
+ svc = BERTRPC::Service.new('localhost', 8000)
77
+ svc.call.calc.add(1, 2)
78
+ # => 3
79
+
80
+
81
+ Contribute
82
+ ----------
83
+
84
+ If you'd like to hack on Ernie, start by forking my repo on GitHub:
85
+
86
+ http://github.com/mojombo/ernie
87
+
88
+ To get all of the dependencies, install the gem first. The best way to get
89
+ your changes merged back into core is as follows:
90
+
91
+ 1. Clone down your fork
92
+ 1. Create a topic branch to contain your change
93
+ 1. Hack away
94
+ 1. Add tests and make sure everything still passes by running `rake`
95
+ 1. If you are adding new functionality, document it in the README.md
96
+ 1. Do not change the version number, I will do that on my end
97
+ 1. If necessary, rebase your commits into logical chunks, without errors
98
+ 1. Push the branch up to GitHub
99
+ 1. Send me (mojombo) a pull request for your branch
100
+
101
+
102
+ Copyright
103
+ ---------
104
+
105
+ Copyright (c) 2009 Tom Preston-Werner. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,65 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "ernie"
8
+ gem.summary = %Q{Ernie is a BERT-RPC server implementation.}
9
+ gem.email = "tom@mojombo.com"
10
+ gem.homepage = "http://github.com/mojombo/ernie"
11
+ gem.authors = ["Tom Preston-Werner"]
12
+ gem.files.include(["ext"])
13
+ gem.extensions << 'ext/extconf.rb'
14
+ gem.add_dependency('erlectricity', '>= 1.0.1')
15
+ gem.add_dependency('bertrpc', '>= 0.2.0')
16
+
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ rescue LoadError
20
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
21
+ end
22
+
23
+ require 'rake/testtask'
24
+ Rake::TestTask.new(:test) do |test|
25
+ test.libs << 'lib' << 'test'
26
+ test.pattern = 'test/**/*_test.rb'
27
+ test.verbose = true
28
+ end
29
+
30
+ begin
31
+ require 'rcov/rcovtask'
32
+ Rcov::RcovTask.new do |test|
33
+ test.libs << 'test'
34
+ test.pattern = 'test/**/*_test.rb'
35
+ test.verbose = true
36
+ end
37
+ rescue LoadError
38
+ task :rcov do
39
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
40
+ end
41
+ end
42
+
43
+ task :default => :test
44
+
45
+ # require 'rake/rdoctask'
46
+ # Rake::RDocTask.new do |rdoc|
47
+ # if File.exist?('VERSION.yml')
48
+ # config = YAML.load(File.read('VERSION.yml'))
49
+ # version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
50
+ # else
51
+ # version = ""
52
+ # end
53
+ #
54
+ # rdoc.rdoc_dir = 'rdoc'
55
+ # rdoc.title = "ernie #{version}"
56
+ # rdoc.rdoc_files.include('README*')
57
+ # rdoc.rdoc_files.include('lib/**/*.rb')
58
+ # end
59
+
60
+ task :ebuild do
61
+ ERLC_TEST_FLAGS = ""
62
+ ERLC_FLAGS = "-o ../ebin"
63
+ cd "elib"
64
+ sh "erlc #{ERLC_FLAGS} #{ERLC_TEST_FLAGS} #{Dir["**/*.erl"].join(" ")}"
65
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 0
4
+ :minor: 3
data/bin/ernie ADDED
@@ -0,0 +1,97 @@
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
+
20
+ help = <<HELP
21
+ Ernie is an Erlang/Ruby BERT-RPC Server.
22
+
23
+ Basic Command Line Usage:
24
+ ernie [options] <path to handler file>
25
+
26
+ Options:
27
+ HELP
28
+
29
+ options = {}
30
+ OptionParser.new do |opts|
31
+ opts.banner = help
32
+
33
+ opts.on("-h HANDLER", "--handler HANDLER", "Handler ruby file") do |x|
34
+ options[:handler] = x
35
+ end
36
+
37
+ opts.on("-p PORT", "--port PORT", "Port") do |x|
38
+ options[:port] = x
39
+ end
40
+
41
+ opts.on("-n NUMBER", "--number NUMBER", "Number of handler instances") do |x|
42
+ options[:number] = x
43
+ end
44
+
45
+ opts.on("-l LOGLEVEL", "--log-level LOGLEVEL", "Log level (0-4)") do |x|
46
+ options[:log_level] = x
47
+ end
48
+
49
+ opts.on("-d", "--detached", "Run as a daemon") do
50
+ options[:detached] = true
51
+ end
52
+
53
+ opts.on("-P", "--pidfile PIDFILE", "Location to write pid file.") do |x|
54
+ options[:pidfile] = x
55
+ end
56
+ end.parse!
57
+
58
+ if command = ARGV[0]
59
+ if !%w{reload-handlers}.include?(command)
60
+ puts "Invlalid command. Valid commands are:"
61
+ puts " reload-handlers"
62
+ exit(1)
63
+ end
64
+
65
+ require 'rubygems'
66
+ require 'bertrpc'
67
+ port = options[:port] || DEFAULT_PORT
68
+ svc = BERTRPC::Service.new('localhost', port)
69
+ puts svc.call.__admin__.send(command.gsub(/-/, '_'))
70
+ else
71
+ if !options[:handler]
72
+ puts "A handler must be specified: ernie -h /path/to/handler.rb"
73
+ exit(1)
74
+ end
75
+
76
+ handler = options[:handler]
77
+ port = options[:port] || DEFAULT_PORT
78
+ number = options[:number] || 1
79
+ log_level = options[:log_level] || 2
80
+ pidfile = options[:pidfile] ? "-ernie_server_app pidfile \"'#{options[:pidfile]}'\"" : ''
81
+ detached = options[:detached] ? '-detached' : ''
82
+
83
+ cmd = %Q{erl -boot start_sasl \
84
+ #{detached} \
85
+ +Bc \
86
+ +K true \
87
+ -smp enable \
88
+ #{code_paths}
89
+ #{pidfile} \
90
+ -ernie_server_app port #{port} \
91
+ -ernie_server_app handler '"#{handler}"' \
92
+ -ernie_server_app number #{number} \
93
+ -ernie_server_app log_level #{log_level} \
94
+ -run ernie_server_app boot}.squeeze(' ')
95
+ puts cmd
96
+ exec(cmd)
97
+ end
@@ -0,0 +1,2 @@
1
+ {application, ernie_server_app,
2
+ [{mod, {ernie_server_app, []}}]}.
@@ -0,0 +1,137 @@
1
+ -module(asset_pool).
2
+ -behaviour(gen_server).
3
+
4
+ %% api
5
+ -export([start_link/1, start/1, lease/0, return/1, reload_assets/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
+ -record(state, {assets = undefined,
12
+ handler = undefined,
13
+ token = undefined}).
14
+
15
+ %%====================================================================
16
+ %% API
17
+ %%====================================================================
18
+
19
+ start_link(Args) ->
20
+ gen_server:start_link({global, ?MODULE}, ?MODULE, Args, []).
21
+
22
+ start(Args) ->
23
+ gen_server:start({global, ?MODULE}, ?MODULE, Args, []).
24
+
25
+ lease() ->
26
+ gen_server:call({global, ?MODULE}, {lease}).
27
+
28
+ return(Asset) ->
29
+ gen_server:call({global, ?MODULE}, {return, Asset}).
30
+
31
+ reload_assets() ->
32
+ gen_server:call({global, ?MODULE}, {reload_assets}).
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([Count, Handler]) ->
46
+ process_flag(trap_exit, true),
47
+ error_logger:info_msg("~p starting~n", [?MODULE]),
48
+ Token = make_ref(),
49
+ Assets = start_handlers(Count, Handler, Token),
50
+ {ok, #state{assets = Assets, handler = Handler, token = Token}}.
51
+
52
+ %%--------------------------------------------------------------------
53
+ %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
54
+ %% {reply, Reply, State, Timeout} |
55
+ %% {noreply, State} |
56
+ %% {noreply, State, Timeout} |
57
+ %% {stop, Reason, Reply, State} |
58
+ %% {stop, Reason, State}
59
+ %% Description: Handling call messages
60
+ %%--------------------------------------------------------------------
61
+ handle_call({lease}, _From, State) ->
62
+ Token = State#state.token,
63
+ case queue:out(State#state.assets) of
64
+ {{value, Asset}, Assets2} ->
65
+ {asset, Port, AssetToken} = Asset,
66
+ case AssetToken =:= Token of
67
+ false ->
68
+ port_wrapper:close(Port),
69
+ Handler = State#state.handler,
70
+ NewAsset = create_asset(Handler, Token);
71
+ true ->
72
+ NewAsset = Asset
73
+ end,
74
+ {reply, {ok, NewAsset}, State#state{assets = Assets2}};
75
+ {empty, _Assets2} ->
76
+ {reply, empty, State}
77
+ end;
78
+ handle_call({return, Asset}, _From, State) ->
79
+ Token = State#state.token,
80
+ {asset, Port, AssetToken} = Asset,
81
+ case AssetToken =:= Token of
82
+ false ->
83
+ port_wrapper:close(Port),
84
+ Handler = State#state.handler,
85
+ NewAsset = create_asset(Handler, Token);
86
+ true ->
87
+ NewAsset = Asset
88
+ end,
89
+ Assets2 = queue:in(NewAsset, State#state.assets),
90
+ {reply, ok, State#state{assets = Assets2}};
91
+ handle_call({reload_assets}, _From, State) ->
92
+ Token = make_ref(),
93
+ {reply, ok, State#state{token = Token}};
94
+ handle_call(_Request, _From, State) ->
95
+ {reply, ok, State}.
96
+
97
+ %%--------------------------------------------------------------------
98
+ %% Function: handle_cast(Msg, State) -> {noreply, State} |
99
+ %% {noreply, State, Timeout} |
100
+ %% {stop, Reason, State}
101
+ %% Description: Handling cast messages
102
+ %%--------------------------------------------------------------------
103
+ handle_cast(_Msg, State) -> {noreply, State}.
104
+
105
+ handle_info({'EXIT', _Pid, normal}, State) ->
106
+ {noreply, State};
107
+ handle_info({'EXIT', Pid, Error}, State) ->
108
+ error_logger:error_msg("Port ~p closed with ~p, restarting port...~n", [Pid, Error]),
109
+ ValidAssets = queue:filter(fun(Item) -> {asset, A, _T} = Item, A =/= Pid end, State#state.assets),
110
+ Handler = State#state.handler,
111
+ Token = State#state.token,
112
+ NewAsset = create_asset(Handler, Token),
113
+ Assets = queue:in(NewAsset, ValidAssets),
114
+ {noreply, State#state{assets = Assets}};
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
+ start_handlers(Count, Handler, Token) ->
127
+ start_handlers(queue:new(), Count, Handler, Token).
128
+
129
+ start_handlers(Assets, 0, _Handler, _Token) ->
130
+ Assets;
131
+ start_handlers(Assets, Count, Handler, Token) ->
132
+ Asset = create_asset(Handler, Token),
133
+ Assets2 = queue:in(Asset, Assets),
134
+ start_handlers(Assets2, Count - 1, Handler, Token).
135
+
136
+ create_asset(Handler, Token) ->
137
+ {asset, port_wrapper:wrap_link("ruby " ++ Handler), Token}.
@@ -0,0 +1,15 @@
1
+ -module(asset_pool_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, Handler} = application:get_env(ernie_server_app, handler),
10
+ io:format("Using handler ~p~n", [Handler]),
11
+ {ok, Number} = application:get_env(ernie_server_app, number),
12
+ io:format("Using ~p handler instances~n", [Number]),
13
+ {ok, {{one_for_one, 1, 60},
14
+ [{asset_pool, {asset_pool, start_link, [[Number, Handler]]},
15
+ permanent, brutal_kill, worker, [asset_pool]}]}}.
@@ -0,0 +1,175 @@
1
+ -module(ernie_server).
2
+ -behaviour(gen_server).
3
+
4
+ %% api
5
+ -export([start_link/1, start/1, process/1, asset_freed/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
+ -record(state, {lsock = undefined,
12
+ pending = queue:new()}).
13
+
14
+ %%====================================================================
15
+ %% API
16
+ %%====================================================================
17
+
18
+ start_link(Args) ->
19
+ gen_server:start_link({global, ?MODULE}, ?MODULE, Args, []).
20
+
21
+ start(Args) ->
22
+ gen_server:start({global, ?MODULE}, ?MODULE, Args, []).
23
+
24
+ process(Sock) ->
25
+ gen_server:cast({global, ?MODULE}, {process, Sock}).
26
+
27
+ asset_freed() ->
28
+ gen_server:cast({global, ?MODULE}, {asset_freed}).
29
+
30
+ %%====================================================================
31
+ %% gen_server callbacks
32
+ %%====================================================================
33
+
34
+ %%--------------------------------------------------------------------
35
+ %% Function: init(Args) -> {ok, State} |
36
+ %% {ok, State, Timeout} |
37
+ %% ignore |
38
+ %% {stop, Reason}
39
+ %% Description: Initiates the server
40
+ %%--------------------------------------------------------------------
41
+ init([Port]) ->
42
+ process_flag(trap_exit, true),
43
+ error_logger:info_msg("~p starting~n", [?MODULE]),
44
+ {ok, LSock} = try_listen(Port, 500),
45
+ spawn(fun() -> loop(LSock) end),
46
+ {ok, #state{lsock = LSock}}.
47
+
48
+ %%--------------------------------------------------------------------
49
+ %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
50
+ %% {reply, Reply, State, Timeout} |
51
+ %% {noreply, State} |
52
+ %% {noreply, State, Timeout} |
53
+ %% {stop, Reason, Reply, State} |
54
+ %% {stop, Reason, State}
55
+ %% Description: Handling call messages
56
+ %%--------------------------------------------------------------------
57
+ handle_call(_Request, _From, State) ->
58
+ {reply, ok, State}.
59
+
60
+ %%--------------------------------------------------------------------
61
+ %% Function: handle_cast(Msg, State) -> {noreply, State} |
62
+ %% {noreply, State, Timeout} |
63
+ %% {stop, Reason, State}
64
+ %% Description: Handling cast messages
65
+ %%--------------------------------------------------------------------
66
+ handle_cast({process, Sock}, State) ->
67
+ case gen_tcp:recv(Sock, 0) of
68
+ {ok, BinaryTerm} ->
69
+ logger:debug("Got binary term: ~p~n", [BinaryTerm]),
70
+ Term = binary_to_term(BinaryTerm),
71
+ logger:info("Got term: ~p~n", [Term]),
72
+ case Term of
73
+ {call, '__admin__', Fun, Args} ->
74
+ State2 = process_admin(Sock, Fun, Args, State);
75
+ Any ->
76
+ State2 = process_normal(BinaryTerm, Sock, State)
77
+ end,
78
+ {noreply, State2};
79
+ {error, closed} ->
80
+ ok = gen_tcp:close(Sock),
81
+ {noreply, State}
82
+ end;
83
+ handle_cast({asset_freed}, State) ->
84
+ case queue:is_empty(State#state.pending) of
85
+ false ->
86
+ case asset_pool:lease() of
87
+ {ok, Asset} ->
88
+ {{value, {pending, BinaryTerm, Sock}}, Pending2} = queue:out(State#state.pending),
89
+ % io:format("d", []),
90
+ spawn(fun() -> process_now(BinaryTerm, Sock, Asset) end),
91
+ {noreply, State#state{pending = Pending2}};
92
+ empty ->
93
+ % io:format(".", []),
94
+ {noreply, State}
95
+ end;
96
+ true ->
97
+ {noreply, State}
98
+ end;
99
+ handle_cast(_Msg, State) -> {noreply, State}.
100
+
101
+ handle_info(Msg, State) ->
102
+ error_logger:error_msg("Unexpected message: ~p~n", [Msg]),
103
+ {noreply, State}.
104
+
105
+ terminate(_Reason, _State) -> ok.
106
+ code_change(_OldVersion, State, _Extra) -> {ok, State}.
107
+
108
+ %%====================================================================
109
+ %% Internal
110
+ %%====================================================================
111
+
112
+ try_listen(Port, 0) ->
113
+ error_logger:error_msg("Could not listen on port ~p~n", [Port]),
114
+ {error, "Could not listen on port"};
115
+ try_listen(Port, Times) ->
116
+ Res = gen_tcp:listen(Port, [binary, {packet, 4}, {active, false}, {reuseaddr, true}]),
117
+ case Res of
118
+ {ok, LSock} ->
119
+ error_logger:info_msg("Listening on port ~p~n", [Port]),
120
+ {ok, LSock};
121
+ {error, Reason} ->
122
+ error_logger:info_msg("Could not listen on port ~p: ~p~n", [Port, Reason]),
123
+ timer:sleep(5000),
124
+ try_listen(Port, Times - 1)
125
+ end.
126
+
127
+ loop(LSock) ->
128
+ {ok, Sock} = gen_tcp:accept(LSock),
129
+ logger:debug("Accepted socket: ~p~n", [Sock]),
130
+ ernie_server:process(Sock),
131
+ loop(LSock).
132
+
133
+ process_admin(Sock, reload_handlers, _Args, State) ->
134
+ asset_pool:reload_assets(),
135
+ gen_tcp:send(Sock, term_to_binary({reply, <<"Handlers reloaded.">>})),
136
+ ok = gen_tcp:close(Sock),
137
+ State;
138
+ process_admin(Sock, Fun, _Args, State) ->
139
+ gen_tcp:send(Sock, term_to_binary({reply, <<"Admin function not supported.">>})),
140
+ ok = gen_tcp:close(Sock),
141
+ State.
142
+
143
+ process_normal(BinaryTerm, Sock, State) ->
144
+ case queue:is_empty(State#state.pending) of
145
+ false ->
146
+ Pending2 = queue:in({pending, BinaryTerm, Sock}, State#state.pending),
147
+ % io:format("Q", []),
148
+ State#state{pending = Pending2};
149
+ true ->
150
+ try_process_now(BinaryTerm, Sock, State)
151
+ end.
152
+
153
+ try_process_now(BinaryTerm, Sock, State) ->
154
+ case asset_pool:lease() of
155
+ {ok, Asset} ->
156
+ % io:format("i", []),
157
+ spawn(fun() -> process_now(BinaryTerm, Sock, Asset) end),
158
+ State;
159
+ empty ->
160
+ % io:format("q", []),
161
+ Pending2 = queue:in({pending, BinaryTerm, Sock}, State#state.pending),
162
+ State#state{pending = Pending2}
163
+ end.
164
+
165
+ process_now(BinaryTerm, Sock, Asset) ->
166
+ % io:format(".", []),
167
+ % error_logger:info_msg("From Internet: ~p~n", [BinaryTerm]),
168
+ {asset, Port, Token} = Asset,
169
+ logger:debug("Asset: ~p ~p~n", [Port, Token]),
170
+ {ok, Data} = port_wrapper:rpc(Port, BinaryTerm),
171
+ % error_logger:info_msg("From Port: ~p~n", [Data]),
172
+ asset_pool:return(Asset),
173
+ ernie_server:asset_freed(),
174
+ gen_tcp:send(Sock, Data),
175
+ ok = gen_tcp:close(Sock).
@@ -0,0 +1,15 @@
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
+ logger_sup:start_link(),
11
+ ernie_server_sup:start_link(),
12
+ asset_pool_sup:start_link().
13
+
14
+ stop(_State) ->
15
+ ok.
@@ -0,0 +1,19 @@
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, {{one_for_one, 1, 60},
18
+ [{ernie_server, {ernie_server, start_link, [[Port]]},
19
+ permanent, brutal_kill, worker, [ernie_server]}]}}.
data/elib/logger.erl ADDED
@@ -0,0 +1,108 @@
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({global, ?MODULE}, ?MODULE, Args, []).
19
+
20
+ start(Args) ->
21
+ gen_server:start({global, ?MODULE}, ?MODULE, Args, []).
22
+
23
+ set_log_level(Level) ->
24
+ gen_server:call({global, ?MODULE}, {set_log_level, Level}).
25
+
26
+ debug(Msg, Args) ->
27
+ gen_server:cast({global, ?MODULE}, {debug, Msg, Args}).
28
+
29
+ info(Msg, Args) ->
30
+ gen_server:cast({global, ?MODULE}, {info, Msg, Args}).
31
+
32
+ warn(Msg, Args) ->
33
+ gen_server:cast({global, ?MODULE}, {warn, Msg, Args}).
34
+
35
+ error(Msg, Args) ->
36
+ gen_server:cast({global, ?MODULE}, {error, Msg, Args}).
37
+
38
+ fatal(Msg, Args) ->
39
+ gen_server:cast({global, ?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.
@@ -0,0 +1,13 @@
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]}]}}.
@@ -0,0 +1,67 @@
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
+ after 15000 ->
19
+ {error, timed_out, WrappedPort}
20
+ end.
21
+
22
+ send(WrappedPort, Message) ->
23
+ WrappedPort ! {self(), {command, Message}},
24
+ WrappedPort.
25
+
26
+ shutdown(WrappedPort) ->
27
+ WrappedPort ! shutdown,
28
+ true.
29
+
30
+ close(WrappedPort) ->
31
+ WrappedPort ! noose,
32
+ true.
33
+
34
+ create_port(Command) ->
35
+ open_port({spawn, Command}, [{packet, 4}, nouse_stdio, exit_status, binary]).
36
+
37
+ loop(Port, Timeout, Command) ->
38
+ receive
39
+ noose ->
40
+ port_close(Port),
41
+ noose;
42
+ shutdown ->
43
+ port_close(Port),
44
+ exit(shutdown);
45
+ {Source, {command, Message}} ->
46
+ Port ! {self(), {command, Message}},
47
+ receive
48
+ {Port, {data, Result}} ->
49
+ Source ! {self(), Result}
50
+ after Timeout ->
51
+ error_logger:error_msg("Port Wrapper ~p timed out in mid operation (~p)!~n", [self(),Message]),
52
+ % We timed out, which means we need to close and then restart the port
53
+ port_close(Port), % Should SIGPIPE the child.
54
+ exit(timed_out)
55
+ end,
56
+ loop(Port,Timeout,Command);
57
+ {Port, {exit_status, _Code}} ->
58
+ % Hard and Unanticipated Crash
59
+ error_logger:error_msg( "Port closed! ~p~n", [Port] ),
60
+ exit({error, _Code});
61
+ {'EXIT',_Pid,shutdown} ->
62
+ port_close(Port),
63
+ exit(shutdown);
64
+ Any ->
65
+ error_logger:warning_msg("PortWrapper ~p got unexpected message: ~p~n", [self(), Any]),
66
+ loop(Port, Timeout, Command)
67
+ end.
data/ernie.gemspec ADDED
@@ -0,0 +1,71 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{ernie}
5
+ s.version = "0.3.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Tom Preston-Werner"]
9
+ s.date = %q{2009-08-14}
10
+ s.default_executable = %q{ernie}
11
+ s.email = %q{tom@mojombo.com}
12
+ s.executables = ["ernie"]
13
+ s.extensions = ["ext/extconf.rb"]
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.md"
17
+ ]
18
+ s.files = [
19
+ ".document",
20
+ ".gitignore",
21
+ "LICENSE",
22
+ "README.md",
23
+ "Rakefile",
24
+ "VERSION.yml",
25
+ "bin/ernie",
26
+ "ebin/ernie_server_app.app",
27
+ "elib/asset_pool.erl",
28
+ "elib/asset_pool_sup.erl",
29
+ "elib/ernie_server.erl",
30
+ "elib/ernie_server_app.erl",
31
+ "elib/ernie_server_sup.erl",
32
+ "elib/logger.erl",
33
+ "elib/logger_sup.erl",
34
+ "elib/port_wrapper.erl",
35
+ "ernie.gemspec",
36
+ "examples/calc.rb",
37
+ "ext/Makefile",
38
+ "ext/extconf.rb",
39
+ "lib/ernie.rb",
40
+ "test/ernie_test.rb",
41
+ "test/load.rb",
42
+ "test/test_helper.rb"
43
+ ]
44
+ s.homepage = %q{http://github.com/mojombo/ernie}
45
+ s.rdoc_options = ["--charset=UTF-8"]
46
+ s.require_paths = ["lib"]
47
+ s.rubygems_version = %q{1.3.5}
48
+ s.summary = %q{Ernie is a BERT-RPC server implementation.}
49
+ s.test_files = [
50
+ "test/ernie_test.rb",
51
+ "test/load.rb",
52
+ "test/test_helper.rb",
53
+ "examples/calc.rb"
54
+ ]
55
+
56
+ if s.respond_to? :specification_version then
57
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
58
+ s.specification_version = 3
59
+
60
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
61
+ s.add_runtime_dependency(%q<erlectricity>, [">= 1.0.1"])
62
+ s.add_runtime_dependency(%q<bertrpc>, [">= 0.2.0"])
63
+ else
64
+ s.add_dependency(%q<erlectricity>, [">= 1.0.1"])
65
+ s.add_dependency(%q<bertrpc>, [">= 0.2.0"])
66
+ end
67
+ else
68
+ s.add_dependency(%q<erlectricity>, [">= 1.0.1"])
69
+ s.add_dependency(%q<bertrpc>, [">= 0.2.0"])
70
+ end
71
+ end
data/examples/calc.rb ADDED
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'ernie'
3
+
4
+ mod(:calc) do
5
+ fun(:add) do |a, b|
6
+ a + b
7
+ end
8
+ end
data/ext/Makefile ADDED
@@ -0,0 +1,2 @@
1
+ install:
2
+ erlc -o ../ebin ../elib/*.erl
data/ext/extconf.rb ADDED
@@ -0,0 +1 @@
1
+ # does nothing, Makefile is handwritten
data/lib/ernie.rb ADDED
@@ -0,0 +1,126 @@
1
+ require 'rubygems'
2
+ require 'erlectricity'
3
+ require 'logger'
4
+
5
+ class Ernie
6
+ class << self
7
+ attr_accessor :mods, :current_mod, :logger
8
+ end
9
+
10
+ self.mods = {}
11
+ self.current_mod = nil
12
+ self.logger = nil
13
+
14
+ def self.mod(name, block)
15
+ m = Mod.new(name)
16
+ self.current_mod = m
17
+ self.mods[name] = m
18
+ block.call
19
+ end
20
+
21
+ def self.fun(name, block)
22
+ self.current_mod.fun(name, block)
23
+ end
24
+
25
+ def self.logfile(file)
26
+ self.logger = Logger.new(file)
27
+ end
28
+
29
+ def self.log(text)
30
+ self.logger.info(text) if self.logger
31
+ end
32
+
33
+ def self.convert(item)
34
+ if item.instance_of?(Hash)
35
+ a = [:dict]
36
+ item.each_pair { |k, v| a << [convert(k), convert(v)] }
37
+ a
38
+ elsif item.instance_of?(Array)
39
+ item.map { |x| convert(x) }
40
+ else
41
+ item
42
+ end
43
+ end
44
+
45
+ def self.deconvert(item)
46
+ if item.instance_of?(Array)
47
+ if item.first == :dict
48
+ item[1..-1].inject({}) do |acc, x|
49
+ acc[deconvert(x[0])] = deconvert(x[1]); acc
50
+ end
51
+ else
52
+ item.map { |x| deconvert(x) }
53
+ end
54
+ else
55
+ item
56
+ end
57
+ end
58
+
59
+ def self.dispatch(mod, fun, args)
60
+ xargs = deconvert(args)
61
+ self.log("-- " + [mod, fun, xargs].inspect)
62
+ res = self.mods[mod].funs[fun].call(*xargs)
63
+ convert(res)
64
+ end
65
+
66
+ def self.start
67
+ self.log("Starting")
68
+ self.log(self.mods.inspect)
69
+ receive do |f|
70
+ f.when([:call, Symbol, Symbol, Array]) do |mod, fun, args|
71
+ self.log("-> " + [:call, mod, fun, args].inspect)
72
+ begin
73
+ res = self.dispatch(mod, fun, args)
74
+ xres = [:reply, res]
75
+ self.log("<- " + xres.inspect)
76
+ f.send!(xres)
77
+ rescue Object => e
78
+ xres = [:error, [:user, 0, e.message]]
79
+ self.log("<- " + xres.inspect)
80
+ self.log(e.backtrace.join("\n"))
81
+ f.send!(xres)
82
+ end
83
+ f.receive_loop
84
+ end
85
+
86
+ f.when(Any) do |any|
87
+ self.log("-> " + any.inspect)
88
+ xres = [:error, [:server, 0, "Invalid request: #{any.inspect}"]]
89
+ self.log("<- " + xres.inspect)
90
+ f.send!(xres)
91
+ f.receive_loop
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ class Ernie::Mod
98
+ attr_accessor :name, :funs
99
+
100
+ def initialize(name)
101
+ self.name = name
102
+ self.funs = {}
103
+ end
104
+
105
+ def fun(name, block)
106
+ self.funs[name] = block
107
+ end
108
+ end
109
+
110
+ # Root level calls
111
+
112
+ def mod(name, &block)
113
+ Ernie.mod(name, block)
114
+ end
115
+
116
+ def fun(name, &block)
117
+ Ernie.fun(name, block)
118
+ end
119
+
120
+ def logfile(name)
121
+ Ernie.logfile(name)
122
+ end
123
+
124
+ at_exit do
125
+ Ernie.start unless $test
126
+ end
@@ -0,0 +1,18 @@
1
+ require 'test_helper'
2
+
3
+ class ErnieTest < Test::Unit::TestCase
4
+ context "mod" do
5
+ should "add a mod to the mods hash" do
6
+ mod(:foo) { }
7
+ assert Ernie.mods[:foo]
8
+ assert Ernie.mods[:foo].instance_of?(Ernie::Mod)
9
+ end
10
+ end
11
+
12
+ context "fun" do
13
+ should "add a fun to the funs hash of the mod" do
14
+ mod(:foo) { fun(:bar) { } }
15
+ assert Ernie.mods[:foo].funs[:bar]
16
+ end
17
+ end
18
+ end
data/test/load.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'bertrpc'
2
+
3
+ threads = []
4
+ svc = BERTRPC::Service.new('localhost', 8000)
5
+
6
+ 5.times do
7
+ threads << Thread.new do
8
+ i = 0
9
+ 100.times { i += svc.call.calc.add(1, 2) }
10
+ print "#{i}\n"
11
+ end
12
+ end
13
+
14
+ threads.each { |t| t.join }
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'ernie'
8
+
9
+ class Test::Unit::TestCase
10
+ end
11
+
12
+ $test = true
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ernie
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - Tom Preston-Werner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-14 00:00:00 -07:00
13
+ default_executable: ernie
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: erlectricity
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.1
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: bertrpc
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.0
34
+ version:
35
+ description:
36
+ email: tom@mojombo.com
37
+ executables:
38
+ - ernie
39
+ extensions:
40
+ - ext/extconf.rb
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.md
44
+ files:
45
+ - .document
46
+ - .gitignore
47
+ - LICENSE
48
+ - README.md
49
+ - Rakefile
50
+ - VERSION.yml
51
+ - bin/ernie
52
+ - ebin/ernie_server_app.app
53
+ - elib/asset_pool.erl
54
+ - elib/asset_pool_sup.erl
55
+ - elib/ernie_server.erl
56
+ - elib/ernie_server_app.erl
57
+ - elib/ernie_server_sup.erl
58
+ - elib/logger.erl
59
+ - elib/logger_sup.erl
60
+ - elib/port_wrapper.erl
61
+ - ernie.gemspec
62
+ - examples/calc.rb
63
+ - ext/Makefile
64
+ - ext/extconf.rb
65
+ - lib/ernie.rb
66
+ - test/ernie_test.rb
67
+ - test/load.rb
68
+ - test/test_helper.rb
69
+ has_rdoc: true
70
+ homepage: http://github.com/mojombo/ernie
71
+ licenses: []
72
+
73
+ post_install_message:
74
+ rdoc_options:
75
+ - --charset=UTF-8
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ version:
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0"
89
+ version:
90
+ requirements: []
91
+
92
+ rubyforge_project:
93
+ rubygems_version: 1.3.5
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: Ernie is a BERT-RPC server implementation.
97
+ test_files:
98
+ - test/ernie_test.rb
99
+ - test/load.rb
100
+ - test/test_helper.rb
101
+ - examples/calc.rb