teej-alchemy 1.0.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/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
+