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 +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
|
+
|