ernie 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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