teej-alchemy 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/License.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2008 TJ Murphy
2
+
3
+ ## MIT LICENSE ##
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,62 @@
1
+ = Name
2
+
3
+ Alchemy v. 1.0.1 - a simple, light-weight list caching server
4
+
5
+ = Description
6
+
7
+ Alchemy is fast, simple, and distributed list caching server intended to
8
+ relieve load on relational
9
+ databases. It uses the same scalable, non-blocking architecture that
10
+ Starling (http://github.com/defunkt/starling) is built on. It also speaks
11
+ the Memcache protocol, so any language that has a memcached client can
12
+ operate with Alchemy.
13
+
14
+ = Installation
15
+
16
+ This project is hosted at GitHub:
17
+
18
+ http://github.com/teej/alchemy/tree/master
19
+
20
+ Alchemy can be installed through GitHub gems:
21
+
22
+ gem sources -a http://gems.github.com
23
+ sudo gem install teej-alchemy
24
+
25
+ = Quick Start Usage
26
+
27
+ In a console window start the Alchemy server. By default
28
+ it runs verbosely in the foreground, listening on 127.0.0.1:22122
29
+ and stores its files under /tmp/alchemy. To run it as a daemon:
30
+
31
+ alchemy -d
32
+
33
+ In a new console test the put and get of items in a list:
34
+
35
+ irb
36
+ >> require 'alchemy'
37
+ => true
38
+ >> alchemy = Alchemy.new('127.0.0.1:22122')
39
+ => #<Alchemy:0x203f384 ... >
40
+ >> alchemy.set("my_array", "chunky")
41
+ => nil
42
+ >> alchemy.set("my_array", "bacon")
43
+ => nil
44
+ >> alchemy.get("my_array")
45
+ => ["chunky", "bacon"]
46
+
47
+ = Authors
48
+
49
+ * TJ Murphy
50
+
51
+ = Starling Contributors
52
+
53
+ * Blaine Cook
54
+ * Chris Wanstrath
55
+ * AnotherBritt
56
+ * Glenn Rempe
57
+ * Abdul-Rahman Advany
58
+
59
+ = Copyright
60
+
61
+ Alchemy - a simple, light-weight list caching server.
62
+ Copyright 2008 TJ Murphy
data/alchemy.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "alchemy"
3
+ s.version = "1.0.1"
4
+ s.date = "2008-10-17"
5
+ s.summary = "A simple, light-weight list caching server"
6
+ s.email = "teej.murphy@gmail.com"
7
+ s.homepage = "http://github.com/teej/alchemy"
8
+ s.description = "Alchemy is fast, simple, and distributed list caching server intended to relieve load on relational databases,"
9
+ s.has_rdoc = true
10
+ s.authors = ["TJ Murphy"]
11
+ s.files = ["License.txt",
12
+ "README.rdoc",
13
+ "alchemy.gemspec",
14
+ "init.rb",
15
+ "bin/alchemy",
16
+ "lib/alchemy.rb",
17
+ "lib/alchemy/handler.rb",
18
+ "lib/alchemy/phylactery.rb",
19
+ "lib/alchemy/runner.rb",
20
+ "lib/alchemy/server.rb",
21
+ "lib/alchemy/alchemized_by.rb",
22
+ "lib/alchemy/uses_alchemy.rb"]
23
+ s.test_files = []
24
+ s.executables = ["alchemy"]
25
+ s.rdoc_options = ["--main", "README.rdoc"]
26
+ s.extra_rdoc_files = ["README.rdoc"]
27
+ s.add_dependency("json", ["> 1.0.0"])
28
+ s.add_dependency("memcached", [">= 0.11"])
29
+ s.add_dependency("eventmachine", [">= 0.12.2"])
30
+ end
data/bin/alchemy ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'alchemy/runner'
5
+ AlchemyServer::Runner.run
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ ['uses_alchemy', 'alchemized_by'].each do |req|
2
+ require File.join(File.dirname(__FILE__), "lib/alchemy/#{req}")
3
+ end
@@ -0,0 +1,25 @@
1
+ module AlchemizedBy
2
+
3
+ def alchemized_by(association_id, opts={})
4
+
5
+ opts[:with] ||= "#{association_id}_id"
6
+ opts[:with] = opts[:with].to_a
7
+ opts[:on] ||= :id
8
+
9
+ opts[:with].each do |method|
10
+
11
+ alchemy_namespace = "#{self.name.underscore.pluralize.downcase}_#{method}"
12
+ alchemy_listname = "#{method}_list_name"
13
+
14
+ define_method(alchemy_listname) do
15
+ "#{association_id.to_s.camelize}|#{send(method)}|#{alchemy_namespace}"
16
+ end
17
+
18
+ define_method("alchemize_#{method}") do
19
+ ALCHEMY.set(send(alchemy_listname), self.send(opts[:on]))
20
+ end
21
+
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,218 @@
1
+ module AlchemyServer
2
+
3
+ ##
4
+ # This is an internal class that's used by Alchemy::Server to handle the
5
+ # Memcached protocol and act as an interface between the Server and the
6
+ # Recipes.
7
+
8
+ class Handler < EventMachine::Connection
9
+
10
+ ACCEPTED_COMMANDS = ["set", "add", "replace", "append", "prepend", "get", "delete", "flush_all", "version", "verbosity", "quit", "stats"].freeze
11
+
12
+ # ERRORs
13
+ ERR_UNKNOWN_COMMAND = "ERROR\r\n".freeze
14
+ ERR_BAD_CLIENT_FORMAT = "CLIENT_ERROR bad command line format\r\n".freeze
15
+ ERR_SERVER_ISSUE = "SERVER_ERROR %s\r\n"
16
+
17
+ # GET
18
+ GET_COMMAND = /\Aget (.{1,250})\s*\r\n/m
19
+ GET_RESPONSE = "VALUE %s %s %s\r\n%s\r\nEND\r\n".freeze
20
+ GET_RESPONSE_EMPTY = "END\r\n".freeze
21
+
22
+ # SET
23
+ SET_COMMAND = /\A(\w+) (.{1,250}) ([0-9]+) ([0-9]+) ([0-9]+)\r\n/m
24
+ SET_RESPONSE_SUCCESS = "STORED\r\n".freeze
25
+ SET_RESPONSE_FAILURE = "NOT STORED\r\n".freeze
26
+ SET_CLIENT_DATA_ERROR = "CLIENT_ERROR bad data chunk\r\nERROR\r\n".freeze
27
+
28
+ # DELETE
29
+ DELETE_COMMAND = /\Adelete (.{1,250})\s?([0-9]*)\r\n/m
30
+ DELETE_RESPONSE_SUCCESS = "DELETED\r\n".freeze
31
+ DELETE_RESPONSE_FAILURE = "NOT_FOUND\r\n".freeze
32
+
33
+ # FLUSH
34
+ FLUSH_COMMAND = /\Aflush_all\s?([0-9]*)\r\n/m
35
+ FLUSH_RESPONSE = "OK\r\n".freeze
36
+
37
+ # VERSION
38
+ VERSION_COMMAND = /\Aversion\r\n/m
39
+ VERSION_RESPONSE = "VERSION #{VERSION}\r\n".freeze
40
+
41
+ # VERBOSITY
42
+ VERBOSITY_COMMAND = /\Averbosity\r\n/m
43
+ VERBOSITY_RESPONSE = "OK\r\n".freeze
44
+
45
+ # QUIT
46
+ QUIT_COMMAND = /\Aquit\r\n/m
47
+
48
+ # STAT Response
49
+ STATS_COMMAND = /\Astats\r\n/m
50
+ STATS_RESPONSE = "STAT pid %d
51
+ STAT uptime %d
52
+ STAT time %d
53
+ STAT version %s
54
+ STAT rusage_user %0.6f
55
+ STAT rusage_system %0.6f
56
+ STAT curr_items %d
57
+ STAT total_items %d
58
+ STAT bytes %d
59
+ STAT curr_connections %d
60
+ STAT total_connections %d
61
+ STAT cmd_get %d
62
+ STAT cmd_set %d
63
+ STAT get_hits %d
64
+ STAT get_misses %d
65
+ STAT bytes_read %d
66
+ STAT bytes_written %d
67
+ STAT limit_maxbytes %d
68
+ %sEND\r\n".freeze
69
+ LIST_STATS_RESPONSE = "STAT list_%s_items %d
70
+ STAT list_%s_total_items %d
71
+ STAT list_%s_logsize %d
72
+ STAT list_%s_expired_items %d\n".freeze
73
+
74
+ ##
75
+ # Creates a new handler for the MemCache protocol that communicates with a
76
+ # given client.
77
+
78
+ def initialize(options = {})
79
+ @opts = options
80
+ end
81
+
82
+ ##
83
+ # Process incoming commands from the attached client.
84
+
85
+ def post_init
86
+ @server = @opts[:server]
87
+ @expiry_stats = Hash.new(0)
88
+ @expected_length = nil
89
+ @server.stats[:total_connections] += 1
90
+ set_comm_inactivity_timeout @opts[:timeout]
91
+ @list_collection = @opts[:list]
92
+ end
93
+
94
+ def receive_data(incoming)
95
+ data = incoming
96
+
97
+ ## Reject request if command isn't recognized
98
+ if !ACCEPTED_COMMANDS.include?(data.split(" ").first)
99
+ response = respond ERR_UNKNOWN_COMMAND
100
+ elsif request_line = data.slice!(/.*?\r\n/m)
101
+ response = process(request_line, data)
102
+ else
103
+ response = respond ERR_BAD_CLIENT_FORMAT
104
+ end
105
+
106
+ if response
107
+ send_data response
108
+ end
109
+ end
110
+
111
+ def process(request, data)
112
+ case request
113
+ when SET_COMMAND
114
+ set($1, $2, $3, $4, $5.to_i, data)
115
+ when GET_COMMAND
116
+ get($1)
117
+ when STATS_COMMAND
118
+ stats
119
+ when DELETE_COMMAND
120
+ delete($1)
121
+ when FLUSH_COMMAND
122
+ flush_all
123
+ when VERSION_COMMAND
124
+ respond VERSION_RESPONSE
125
+ when VERBOSITY_COMMAND
126
+ respond VERBOSITY_RESPONSE
127
+ when QUIT_COMMAND
128
+ close_connection
129
+ nil
130
+ else
131
+ logger.warn "Bad Format: #{data}."
132
+ respond ERR_BAD_CLIENT_FORMAT
133
+ end
134
+ rescue => e
135
+ logger.error "Error handling request: #{e}."
136
+ logger.debug e.backtrace.join("\n")
137
+ respond ERR_SERVER_ISSUE, e.to_s
138
+ end
139
+
140
+ private
141
+ def respond(str, *args)
142
+ response = sprintf(str, *args)
143
+ @server.stats[:bytes_written] += response.length
144
+ response
145
+ end
146
+
147
+ def set(command, key, flags, expiry, expected_data_size, data)
148
+ data = data.to_s
149
+ respond SET_RESPONSE_FAILURE unless (data.size == expected_data_size + 2)
150
+ data = data[0...expected_data_size]
151
+
152
+ if @list_collection.send(command.to_sym, key, data)
153
+ respond SET_RESPONSE_SUCCESS
154
+ else
155
+ respond SET_RESPONSE_FAILURE
156
+ end
157
+ end
158
+
159
+ def get(key)
160
+ key = key.strip
161
+ if data = @list_collection.get(key)
162
+ respond GET_RESPONSE, key, 0, data.size, data
163
+ else
164
+ respond GET_RESPONSE_EMPTY
165
+ end
166
+ end
167
+
168
+ def delete(key)
169
+ if @list_collection.delete(key)
170
+ respond DELETE_RESPONSE_SUCCESS
171
+ else
172
+ respond DELETE_RESPONSE_FAILURE
173
+ end
174
+ end
175
+
176
+ def flush_all
177
+ @list_collection.flush_all
178
+ respond FLUSH_RESPONSE
179
+ end
180
+
181
+ def stats
182
+ respond STATS_RESPONSE,
183
+ Process.pid, # pid
184
+ Time.now - @server.stats(:start_time), # uptime
185
+ Time.now.to_i, # time
186
+ AlchemyServer::VERSION, # version
187
+ Process.times.utime, # rusage_user
188
+ Process.times.stime, # rusage_system
189
+ @list_collection.stats(:current_size), # curr_items
190
+ @list_collection.stats(:total_items), # total_items
191
+ @list_collection.stats(:current_bytes), # bytes
192
+ @server.stats(:connections), # curr_connections
193
+ @server.stats(:total_connections), # total_connections
194
+ @server.stats(:get_requests), # get count
195
+ @server.stats(:set_requests), # set count
196
+ @list_collection.stats(:get_hits),
197
+ @list_collection.stats(:get_misses),
198
+ @server.stats(:bytes_read), # total bytes read
199
+ @server.stats(:bytes_written), # total bytes written
200
+ 0, # limit_maxbytes
201
+ list_stats
202
+ end
203
+
204
+ def list_stats
205
+ @list_collection.lists.inject("") do |m,(k,v)|
206
+ m + sprintf(LIST_STATS_RESPONSE,
207
+ k, v.length,
208
+ k, v.total_items,
209
+ k, v.logsize,
210
+ k, @expiry_stats[k])
211
+ end
212
+ end
213
+
214
+ def logger
215
+ @server.logger
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,146 @@
1
+ require 'thread'
2
+ require 'json'
3
+
4
+ module AlchemyServer
5
+
6
+ class Phylactery
7
+
8
+ def initialize
9
+ @shutdown_mutex = Mutex.new
10
+ @lists = {}
11
+ @list_init_mutexes = {}
12
+ @stats = Hash.new(0)
13
+ end
14
+
15
+ def set(key, data)
16
+ list = lists(key)
17
+ return false unless list
18
+
19
+ value = value_for_data(data)
20
+ list.push(value)
21
+
22
+ return true
23
+ end
24
+
25
+
26
+ def add(key, data)
27
+ list = lists(key)
28
+ return false unless list
29
+
30
+ value = value_for_data(data)
31
+ return false if list.include?(value)
32
+ list.push(value)
33
+
34
+ return true
35
+ end
36
+
37
+ ## Special. Expects data to be a JSON array
38
+ def replace(key, data)
39
+ list = lists(key)
40
+ return false unless list
41
+ value = JSON.parse(data)
42
+ return false unless value.is_a? Array
43
+ list.replace(value)
44
+
45
+ return true
46
+ end
47
+
48
+ alias :append :set
49
+
50
+ def prepend(key, data)
51
+ list = lists(key)
52
+ return false unless list
53
+
54
+ value = value_for_data(data)
55
+ list.unshift(value)
56
+
57
+ return true
58
+ end
59
+
60
+ ## CAS command not supported
61
+
62
+ def get(key)
63
+ list = lists(key).to_a
64
+ return false if list.empty?
65
+ return list.to_json
66
+ end
67
+
68
+ def gets(keys)
69
+ all_lists = {}
70
+ keys.each { |key| all_lists[key] = lists(key).to_a }
71
+ return all_lists.to_json
72
+ end
73
+
74
+ def delete(key)
75
+ @lists.delete(key)
76
+ end
77
+
78
+ def flush_all
79
+ @lists = {}
80
+ @list_init_mutexes = {}
81
+ end
82
+
83
+ ##
84
+ # Returns all active lists.
85
+
86
+ def lists(key=nil)
87
+ return nil if @shutdown_mutex.locked?
88
+
89
+ return @lists if key.nil?
90
+ # First try to return the list named 'key' if it's available.
91
+ return @lists[key] if @lists[key]
92
+
93
+ @list_init_mutexes[key] ||= Mutex.new
94
+
95
+ if @list_init_mutexes[key].locked?
96
+ return nil
97
+ else
98
+ @list_init_mutexes[key].lock
99
+ if @lists[key].nil?
100
+ @lists[key] = []
101
+ end
102
+ @list_init_mutexes[key].unlock
103
+ end
104
+
105
+ return @lists[key]
106
+ end
107
+
108
+ ##
109
+ # Returns statistic +stat_name+ for the Recipes.
110
+ #
111
+ # Valid statistics are:
112
+ #
113
+ # [:get_misses] Total number of get requests with empty responses
114
+ # [:get_hits] Total number of get requests that returned data
115
+ # [:current_bytes] Current size in bytes of items in the lists
116
+ # [:current_size] Current number of items across all lists
117
+ # [:total_items] Total number of items stored in lists.
118
+
119
+ def stats(stat_name)
120
+ case stat_name
121
+ when nil; @stats
122
+ when :current_size; current_size
123
+ else; @stats[stat_name]
124
+ end
125
+ end
126
+
127
+ ##
128
+ # Safely close all lists.
129
+
130
+ def close
131
+ @shutdown_mutex.lock
132
+ end
133
+
134
+ private
135
+
136
+ def current_size #:nodoc:
137
+ @lists.inject(0) { |m, (k,v)| m + v.length }
138
+ end
139
+
140
+ def value_for_data(data)
141
+ # if the data is an integer, save it as such. otherwise, save as a string
142
+ (data.to_i.to_s == data) ? data.to_i : data
143
+ end
144
+
145
+ end
146
+ end
@@ -0,0 +1,256 @@
1
+ require File.join(File.dirname(__FILE__), 'server')
2
+ require 'optparse'
3
+ puts "Running with Alchemy"
4
+
5
+
6
+ module AlchemyServer
7
+ class Runner
8
+
9
+ attr_accessor :options
10
+ private :options, :options=
11
+
12
+ def self.run
13
+ new
14
+ end
15
+
16
+ def initialize
17
+ parse_options
18
+
19
+ @process = ProcessHelper.new(options[:log_file], options[:pid_file], options[:user], options[:group])
20
+
21
+ pid = @process.running?
22
+ if pid
23
+ STDERR.puts "There is already a alchemy process running (pid #{pid}), exiting."
24
+ exit(1)
25
+ elsif pid.nil?
26
+ STDERR.puts "Cleaning up stale pidfile at #{options[:pid_file]}."
27
+ end
28
+
29
+ start
30
+ end
31
+
32
+ def parse_options
33
+ self.options = { :host => '127.0.0.1',
34
+ :port => 22122,
35
+ :path => File.join(%w( / var spool alchemy )),
36
+ :log_level => 0,
37
+ :daemonize => false,
38
+ :pid_file => File.join(%w( / var run alchemy.pid )) }
39
+
40
+ OptionParser.new do |opts|
41
+ opts.summary_width = 25
42
+
43
+ opts.banner = "alchemy (#{AlchemyServer::VERSION})\n\n",
44
+ "usage: alchemy [-v] [-q path] [-h host] [-p port]\n",
45
+ " [-d [-P pidfile]] [-u user] [-g group] [-l log]\n",
46
+ " alchemy --help\n",
47
+ " alchemy --version\n"
48
+
49
+ opts.separator ""
50
+ opts.separator "Configuration:"
51
+ opts.on("-q", "--list_path PATH",
52
+ :REQUIRED,
53
+ "Path to store alchemy list logs", "(default: #{options[:path]})") do |list_path|
54
+ options[:path] = list_path
55
+ end
56
+
57
+ opts.separator ""; opts.separator "Network:"
58
+
59
+ opts.on("-hHOST", "--host HOST", "Interface on which to listen (default: #{options[:host]})") do |host|
60
+ options[:host] = host
61
+ end
62
+
63
+ opts.on("-pHOST", "--port PORT", Integer, "TCP port on which to listen (default: #{options[:port]})") do |port|
64
+ options[:port] = port
65
+ end
66
+
67
+ opts.separator ""; opts.separator "Process:"
68
+
69
+ opts.on("-d", "Run as a daemon.") do
70
+ options[:daemonize] = true
71
+ end
72
+
73
+ opts.on("-PFILE", "--pid FILE", "save PID in FILE when using -d option.", "(default: #{options[:pid_file]})") do |pid_file|
74
+ options[:pid_file] = pid_file
75
+ end
76
+
77
+ opts.on("-u", "--user USER", Integer, "User to run as") do |user|
78
+ options[:user] = user
79
+ end
80
+
81
+ opts.on("-gGROUP", "--group GROUP", "Group to run as") do |group|
82
+ options[:group] = group
83
+ end
84
+
85
+ opts.separator ""; opts.separator "Logging:"
86
+
87
+ opts.on("-l", "--log [FILE]", "Path to print debugging information.") do |log_path|
88
+ options[:log] = log_path
89
+ end
90
+
91
+ opts.on("-v", "Increase logging verbosity.") do
92
+ options[:log_level] += 1
93
+ end
94
+
95
+ opts.separator ""; opts.separator "Miscellaneous:"
96
+
97
+ opts.on_tail("-?", "--help", "Display this usage information.") do
98
+ puts "#{opts}\n"
99
+ exit
100
+ end
101
+
102
+ opts.on_tail("-V", "--version", "Print version number and exit.") do
103
+ puts "alchemy #{AlchemyServer::VERSION}\n\n"
104
+ exit
105
+ end
106
+ end.parse!
107
+ end
108
+
109
+ def start
110
+ drop_privileges
111
+
112
+ @process.daemonize if options[:daemonize]
113
+
114
+ setup_signal_traps
115
+ @process.write_pid_file
116
+
117
+ STDOUT.puts "Starting at #{options[:host]}:#{options[:port]}."
118
+ @server = AlchemyServer::Base.new(options)
119
+ @server.run
120
+
121
+ @process.remove_pid_file
122
+ end
123
+
124
+ def drop_privileges
125
+ Process.euid = options[:user] if options[:user]
126
+ Process.egid = options[:group] if options[:group]
127
+ end
128
+
129
+ def shutdown
130
+ begin
131
+ STDOUT.puts "Shutting down."
132
+ @server.logger.info "Shutting down."
133
+ @server.stop
134
+ rescue Object => e
135
+ STDERR.puts "There was an error shutting down: #{e}"
136
+ exit(70)
137
+ end
138
+ end
139
+
140
+ def setup_signal_traps
141
+ Signal.trap("INT") { shutdown }
142
+ Signal.trap("TERM") { shutdown }
143
+ end
144
+ end
145
+
146
+ class ProcessHelper
147
+
148
+ def initialize(log_file = nil, pid_file = nil, user = nil, group = nil)
149
+ @log_file = log_file
150
+ @pid_file = pid_file
151
+ @user = user
152
+ @group = group
153
+ end
154
+
155
+ def safefork
156
+ begin
157
+ if pid = fork
158
+ return pid
159
+ end
160
+ rescue Errno::EWOULDBLOCK
161
+ sleep 5
162
+ retry
163
+ end
164
+ end
165
+
166
+ def daemonize
167
+ sess_id = detach_from_terminal
168
+ exit if pid = safefork
169
+
170
+ Dir.chdir("/")
171
+ File.umask 0000
172
+
173
+ close_io_handles
174
+ redirect_io
175
+
176
+ return sess_id
177
+ end
178
+
179
+ def detach_from_terminal
180
+ srand
181
+ safefork and exit
182
+
183
+ unless sess_id = Process.setsid
184
+ raise "Couldn't detache from controlling terminal."
185
+ end
186
+
187
+ trap 'SIGHUP', 'IGNORE'
188
+
189
+ sess_id
190
+ end
191
+
192
+ def close_io_handles
193
+ ObjectSpace.each_object(IO) do |io|
194
+ unless [STDIN, STDOUT, STDERR].include?(io)
195
+ begin
196
+ io.close unless io.closed?
197
+ rescue Exception
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ def redirect_io
204
+ begin; STDIN.reopen('/dev/null'); rescue Exception; end
205
+
206
+ if @log_file
207
+ begin
208
+ STDOUT.reopen(@log_file, "a")
209
+ STDOUT.sync = true
210
+ rescue Exception
211
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
212
+ end
213
+ else
214
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
215
+ end
216
+
217
+ begin; STDERR.reopen(STDOUT); rescue Exception; end
218
+ STDERR.sync = true
219
+ end
220
+
221
+ def rescue_exception
222
+ begin
223
+ yield
224
+ rescue Exception
225
+ end
226
+ end
227
+
228
+ def write_pid_file
229
+ return unless @pid_file
230
+ File.open(@pid_file, "w") { |f| f.write(Process.pid) }
231
+ File.chmod(0644, @pid_file)
232
+ end
233
+
234
+ def remove_pid_file
235
+ return unless @pid_file
236
+ File.unlink(@pid_file) if File.exists?(@pid_file)
237
+ end
238
+
239
+ def running?
240
+ return false unless @pid_file
241
+
242
+ pid = File.read(@pid_file).chomp.to_i rescue nil
243
+ pid = nil if pid == 0
244
+ return false unless pid
245
+
246
+ begin
247
+ Process.kill(0, pid)
248
+ return pid
249
+ rescue Errno::ESRCH
250
+ return nil
251
+ rescue Errno::EPERM
252
+ return pid
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,99 @@
1
+ require 'socket'
2
+ require 'logger'
3
+ require 'rubygems'
4
+ require 'eventmachine'
5
+
6
+ here = File.dirname(__FILE__)
7
+
8
+ require File.join(here, 'phylactery')
9
+ require File.join(here, 'handler')
10
+
11
+ module AlchemyServer
12
+
13
+ VERSION = "1.0.1"
14
+
15
+ class Base
16
+ attr_reader :logger
17
+
18
+ DEFAULT_HOST = '127.0.0.1'
19
+ DEFAULT_PORT = 22122
20
+ DEFAULT_PATH = "/tmp/alchemy/"
21
+ DEFAULT_TIMEOUT = 60
22
+
23
+ ##
24
+ # Initialize a new Alchemy server and immediately start processing
25
+ # requests.
26
+ #
27
+ # +opts+ is an optional hash, whose valid options are:
28
+ #
29
+ # [:host] Host on which to listen (default is 127.0.0.1).
30
+ # [:port] Port on which to listen (default is 22122).
31
+ # [:path] Path to Alchemy list logs. Default is /tmp/alchemy/
32
+ # [:timeout] Time in seconds to wait before closing connections.
33
+ # [:logger] A Logger object, an IO handle, or a path to the log.
34
+ # [:loglevel] Logger verbosity. Default is Logger::ERROR.
35
+ #
36
+ # Other options are ignored.
37
+
38
+ def self.start(opts = {})
39
+ server = self.new(opts)
40
+ server.run
41
+ end
42
+
43
+ ##
44
+ # Initialize a new Alchemy server, but do not accept connections or
45
+ # process requests.
46
+ #
47
+ # +opts+ is as for +start+
48
+
49
+ def initialize(opts = {})
50
+ @opts = {
51
+ :host => DEFAULT_HOST,
52
+ :port => DEFAULT_PORT,
53
+ :path => DEFAULT_PATH,
54
+ :timeout => DEFAULT_TIMEOUT,
55
+ :server => self
56
+ }.merge(opts)
57
+
58
+ @stats = Hash.new(0)
59
+ end
60
+
61
+ ##
62
+ # Start listening and processing requests.
63
+
64
+ def run
65
+ @stats[:start_time] = Time.now
66
+
67
+ @logger = case @opts[:logger]
68
+ when IO, String; Logger.new(@opts[:logger])
69
+ when Logger; @opts[:logger]
70
+ else; Logger.new(STDERR)
71
+ end
72
+
73
+ @opts[:list] = Phylactery.new
74
+ @logger.level = @opts[:log_level] || Logger::ERROR
75
+
76
+ EventMachine.run do
77
+ EventMachine.epoll
78
+ EventMachine.set_descriptor_table_size(4096)
79
+ EventMachine.start_server(@opts[:host], @opts[:port], Handler, @opts)
80
+ end
81
+ end
82
+
83
+ ##
84
+ # Stop accepting new connections and shutdown gracefully.
85
+
86
+ def stop
87
+ @opts[:list].close
88
+ EventMachine.stop_event_loop
89
+ end
90
+
91
+ def stats(stat = nil) #:nodoc:
92
+ case stat
93
+ when nil; @stats
94
+ when :connections; 1
95
+ else; @stats[stat]
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,31 @@
1
+ module UsesAlchemy
2
+
3
+ def uses_alchemy(association_id, opts={})
4
+ opts[:on] ||= "#{self.name.downcase}_id"
5
+ opts[:name] ||= association_id
6
+ opts[:store] ||= :id
7
+
8
+ klass = association_id.to_s.singularize.camelize.constantize
9
+ opts[:proc] ||= Proc.new { |list| list.map{ |aid| klass.find(aid) } }
10
+
11
+ alchemy_namespace = "#{association_id}_#{opts[:on]}"
12
+ alchemy_listname ="#{alchemy_namespace}_list_name"
13
+
14
+ define_method(alchemy_listname) do
15
+ "#{self.class}|#{id}|#{alchemy_namespace}"
16
+ end
17
+
18
+ define_method(opts[:name]) do |*reload|
19
+ list = ALCHEMY.get(send(alchemy_listname)) if !reload.first
20
+
21
+ if reload.first || list.nil?
22
+ refreshed_list = klass.find(:all, :select=>"#{opts[:store]}, #{opts[:on]}", :conditions=>["#{opts[:on]} = ?", self.id])
23
+ refreshed_list = refreshed_list.map(&opts[:store])
24
+ ALCHEMY.replace(send(alchemy_listname), refreshed_list.to_json)
25
+ list = refreshed_list
26
+ end
27
+ opts[:proc].call(list)
28
+ end
29
+ end
30
+
31
+ end
data/lib/alchemy.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'memcached'
2
+ require 'json'
3
+
4
+ class Alchemy < Memcached
5
+ ## SETTERS
6
+ def set(key, value, timeout=0)
7
+ check_return_code(
8
+ Lib.memcached_set(@struct, key, value.to_s, timeout, FLAGS)
9
+ )
10
+ end
11
+
12
+ def add(key, value, timeout=0)
13
+ check_return_code(
14
+ Lib.memcached_add(@struct, key, value.to_s, timeout, FLAGS)
15
+ )
16
+ end
17
+
18
+ def replace(key, value, timeout=0)
19
+ check_return_code(
20
+ Lib.memcached_replace(@struct, key, value.to_a.to_json, timeout, FLAGS)
21
+ )
22
+ end
23
+
24
+ ## GETTER
25
+ def get(keys)#, marshal=true)
26
+ if keys.is_a? Array
27
+ # Multi get
28
+ keys.map! { |key| key }
29
+ hash = {}
30
+
31
+ ret = Lib.memcached_mget(@struct, keys);
32
+ check_return_code(ret)
33
+
34
+ keys.size.times do
35
+ value, key, flags, ret = Lib.memcached_fetch_rvalue(@struct)
36
+ break if ret == Lib::MEMCACHED_END
37
+ check_return_code(ret)
38
+ hash[key] = JSON.parse(value)
39
+ end
40
+ hash
41
+ else
42
+ # Single get
43
+ value, flags, ret = Lib.memcached_get_rvalue(@struct, keys)
44
+ #check_return_code(ret)
45
+ unless value.empty?
46
+ value = JSON.parse(value)
47
+ else
48
+ value = nil
49
+ end
50
+ value
51
+ end
52
+ end
53
+
54
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: teej-alchemy
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - TJ Murphy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-10-17 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">"
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.0
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: memcached
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: "0.11"
32
+ version:
33
+ - !ruby/object:Gem::Dependency
34
+ name: eventmachine
35
+ version_requirement:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.12.2
41
+ version:
42
+ description: Alchemy is fast, simple, and distributed list caching server intended to relieve load on relational databases,
43
+ email: teej.murphy@gmail.com
44
+ executables:
45
+ - alchemy
46
+ extensions: []
47
+
48
+ extra_rdoc_files:
49
+ - README.rdoc
50
+ files:
51
+ - License.txt
52
+ - README.rdoc
53
+ - alchemy.gemspec
54
+ - init.rb
55
+ - bin/alchemy
56
+ - lib/alchemy.rb
57
+ - lib/alchemy/handler.rb
58
+ - lib/alchemy/phylactery.rb
59
+ - lib/alchemy/runner.rb
60
+ - lib/alchemy/server.rb
61
+ - lib/alchemy/alchemized_by.rb
62
+ - lib/alchemy/uses_alchemy.rb
63
+ has_rdoc: true
64
+ homepage: http://github.com/teej/alchemy
65
+ post_install_message:
66
+ rdoc_options:
67
+ - --main
68
+ - README.rdoc
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: "0"
82
+ version:
83
+ requirements: []
84
+
85
+ rubyforge_project:
86
+ rubygems_version: 1.2.0
87
+ signing_key:
88
+ specification_version: 2
89
+ summary: A simple, light-weight list caching server
90
+ test_files: []
91
+