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 +22 -0
- data/README.rdoc +62 -0
- data/alchemy.gemspec +30 -0
- data/bin/alchemy +5 -0
- data/init.rb +3 -0
- data/lib/alchemy/alchemized_by.rb +25 -0
- data/lib/alchemy/handler.rb +218 -0
- data/lib/alchemy/phylactery.rb +146 -0
- data/lib/alchemy/runner.rb +256 -0
- data/lib/alchemy/server.rb +99 -0
- data/lib/alchemy/uses_alchemy.rb +31 -0
- data/lib/alchemy.rb +54 -0
- metadata +91 -0
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
data/init.rb
ADDED
@@ -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
|
+
|