ey_snaplock 0.0.8
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 +20 -0
- data/README.md +21 -0
- data/bin/ey-snaplock +6 -0
- data/lib/ey_snaplock/database/mysql.rb +66 -0
- data/lib/ey_snaplock/database/postgresql9.rb +46 -0
- data/lib/ey_snaplock/database.rb +33 -0
- data/lib/ey_snaplock/version.rb +5 -0
- data/lib/ey_snaplock.rb +84 -0
- metadata +180 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Engine Yard Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
EY Snaplock
|
2
|
+
===========
|
3
|
+
|
4
|
+
almost known as ey-snapshot-really
|
5
|
+
|
6
|
+
lock the filesystem and almost really run the snapshot.
|
7
|
+
|
8
|
+
Postgres testing notes,
|
9
|
+
|
10
|
+
wal_level = hot_standby
|
11
|
+
archive_mode = on
|
12
|
+
archive_command = '/usr/bin/true'
|
13
|
+
|
14
|
+

|
15
|
+
|
16
|
+
Release process:
|
17
|
+
edit version file
|
18
|
+
gem build ey_snaplock.gemspec
|
19
|
+
ey-gem upload ey_snaplock-0.0.5.gem --server public
|
20
|
+
edit version file again
|
21
|
+
commit
|
data/bin/ey-snaplock
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module EY
|
2
|
+
class Snaplock
|
3
|
+
class Database
|
4
|
+
class MySQL
|
5
|
+
def initialize(uri)
|
6
|
+
@mysql = mysql_command(uri)
|
7
|
+
end
|
8
|
+
|
9
|
+
def with_lock(timeout)
|
10
|
+
aquire_lock_within_timeout(timeout)
|
11
|
+
write_master_status(ENV["MASTER_STATUS_FILE"] || "/db/mysql/.snapshot_backup_master_status.txt")
|
12
|
+
yield
|
13
|
+
rescue Timeout::Error
|
14
|
+
lock_timeout(timeout)
|
15
|
+
ensure
|
16
|
+
release_lock
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def write_master_status(file)
|
22
|
+
master_status_cmd = "#{@mysql} -e'SHOW MASTER STATUS\\G' > #{file}"
|
23
|
+
system(master_status_cmd)
|
24
|
+
end
|
25
|
+
|
26
|
+
def aquire_lock_within_timeout(timeout)
|
27
|
+
pipe = IO.popen(@mysql, 'w')
|
28
|
+
@read_lock_pid = pipe.pid
|
29
|
+
pipe.puts('flush tables with read lock;')
|
30
|
+
|
31
|
+
SystemTimer.timeout_after(timeout) do
|
32
|
+
until locked?
|
33
|
+
sleep 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def locked?
|
39
|
+
waiting_read_lock_thread_id == ''
|
40
|
+
end
|
41
|
+
|
42
|
+
def waiting_read_lock_thread_id
|
43
|
+
%x<#{@mysql} -N -e 'show full processlist;' | grep 'flush tables with read lock' | awk '{print $1}'>
|
44
|
+
end
|
45
|
+
|
46
|
+
def lock_timeout(timeout)
|
47
|
+
thread_id = waiting_read_lock_thread_id
|
48
|
+
system("#{@mysql} -e'kill #{thread_id};'") unless thread_id == ''
|
49
|
+
abort "Read lock not acquired after #{timeout} second timeout. Killed request and aborting backup."
|
50
|
+
end
|
51
|
+
|
52
|
+
def release_lock
|
53
|
+
Process.kill('TERM', @read_lock_pid) # unlock tables
|
54
|
+
end
|
55
|
+
|
56
|
+
def mysql_command(uri)
|
57
|
+
command = "mysql"
|
58
|
+
command << " -u" << (uri.user || 'root')
|
59
|
+
command << " -p" << uri.password if uri.password
|
60
|
+
command << " -h" << uri.host if uri.host
|
61
|
+
command
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module EY
|
2
|
+
class Snaplock
|
3
|
+
class Database
|
4
|
+
class Postgresql
|
5
|
+
# Creates an object called @postgresql for usage with IO.popen.
|
6
|
+
def initialize(uri)
|
7
|
+
@postgresql = postgresql_command(uri)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Default function being called (e.g. database.with_lock)
|
11
|
+
def with_lock(timeout) #timeout is ignored
|
12
|
+
aquire_lock
|
13
|
+
yield
|
14
|
+
ensure
|
15
|
+
release_lock
|
16
|
+
end
|
17
|
+
|
18
|
+
# http://wiki.postgresql.org/wiki/Hot_Standby
|
19
|
+
# We don't need to acquire a read lock for the snapshot. However we do need to create an backup of the base database and force a checkpoint to ensure the data is written so when the slave comes up it has enough data to start.
|
20
|
+
def aquire_lock
|
21
|
+
pipe = IO.popen(@postgresql, 'w')
|
22
|
+
@read_lock_pid = pipe.pid
|
23
|
+
pipe.puts("select pg_start_backup('backup',true);")
|
24
|
+
sleep 1
|
25
|
+
pipe.close
|
26
|
+
end
|
27
|
+
|
28
|
+
# The backup should have been started to finish this and actually force the checkpoint we need to stop the backup. This completes the only action that PostgreSQL should require for a hot_standby.
|
29
|
+
def release_lock
|
30
|
+
pipe2 = IO.popen(@postgresql, 'w')
|
31
|
+
@read_lock_pid2 = pipe2.pid
|
32
|
+
pipe2.puts("select pg_stop_backup();")
|
33
|
+
sleep 1
|
34
|
+
pipe2.close
|
35
|
+
end
|
36
|
+
|
37
|
+
# Construct to help prepare the command.
|
38
|
+
def postgresql_command(uri)
|
39
|
+
command = "psql"
|
40
|
+
command << " -U" << (uri.user || 'postgres')
|
41
|
+
command
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'ey_snaplock/database/mysql'
|
3
|
+
require 'ey_snaplock/database/postgresql9'
|
4
|
+
module EY
|
5
|
+
class Snaplock
|
6
|
+
class Database
|
7
|
+
def self.for(database_uri)
|
8
|
+
uri = parse_uri(database_uri)
|
9
|
+
# Determine what kind of database we need to snaplock, current two supported schemes are:
|
10
|
+
# mysql://root:password@localhost/
|
11
|
+
# postgres9:://postgres:localhost/
|
12
|
+
case uri.scheme
|
13
|
+
when 'mysql'
|
14
|
+
MySQL.new(uri)
|
15
|
+
when "postgres9"
|
16
|
+
Postgresql.new(uri)
|
17
|
+
# Unknown url schema provided, print the error message and exit with an exit code of 1
|
18
|
+
else
|
19
|
+
abort "Don't know how to lock database with URI: #{database_uri.inspect}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parse_uri(uri)
|
24
|
+
begin
|
25
|
+
Addressable::URI.parse(uri)
|
26
|
+
rescue TypeError, Addressable::URI::InvalidURIError
|
27
|
+
$stderr.puts "Error parsing database URI: #{uri.inspect}"
|
28
|
+
raise
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/ey_snaplock.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'ey_snaplock/database'
|
4
|
+
require 'system_timer'
|
5
|
+
|
6
|
+
module EY
|
7
|
+
class Snaplock
|
8
|
+
REQUEST_TIMEOUT = 15
|
9
|
+
PER_DATABASE_TIMEOUT = 5
|
10
|
+
|
11
|
+
def self.call(*argv)
|
12
|
+
new(*argv).call
|
13
|
+
exit 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(callback_uri = nil, *database_uris)
|
17
|
+
if callback_uri.nil?
|
18
|
+
$stderr.puts "Usage: ey-snaplock CALLBACK_URI [DATABASE_URI] [DATABASE_URI] ..."
|
19
|
+
abort "Error: CALLBACK_URI is required."
|
20
|
+
end
|
21
|
+
|
22
|
+
callback = request(callback_uri)
|
23
|
+
callback = timeout(&callback)
|
24
|
+
callback = sync_filesystem(&callback)
|
25
|
+
|
26
|
+
database_uris.each do |database_uri| # it'd be nice to parallelize these database locks when we have more than one
|
27
|
+
callback = database_lock(database_uri, PER_DATABASE_TIMEOUT, &callback)
|
28
|
+
end
|
29
|
+
@callback = callback
|
30
|
+
end
|
31
|
+
|
32
|
+
def call
|
33
|
+
success, code, body = @callback.call
|
34
|
+
if success
|
35
|
+
exit
|
36
|
+
else
|
37
|
+
abort "Error: Received unsuccessful response #{code} from callback:\n#{body}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def request(uri)
|
42
|
+
uri = URI.parse(uri)
|
43
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
44
|
+
http.use_ssl = true if uri.scheme == "https"
|
45
|
+
|
46
|
+
lambda do
|
47
|
+
response = http.request_post(post_path_for_uri(uri), "", "Content-Type" => "application/x-www-form-urlencoded")
|
48
|
+
[(200...300).include?(response.code.to_i), response.code.to_i, response.body]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def timeout
|
53
|
+
lambda do
|
54
|
+
begin
|
55
|
+
SystemTimer.timeout_after(REQUEST_TIMEOUT) { yield }
|
56
|
+
rescue Timeout::Error
|
57
|
+
$stderr.puts "Timeout Exceeded: Callback request took longer than #{REQUEST_TIMEOUT} seconds."
|
58
|
+
raise
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def sync_filesystem(&block)
|
64
|
+
lambda do
|
65
|
+
`sync && sync && sync`
|
66
|
+
yield
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def database_lock(database_uri, timeout, &block)
|
71
|
+
database = Database.for(database_uri)
|
72
|
+
lambda do
|
73
|
+
database.with_lock(timeout, &block)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def post_path_for_uri(uri)
|
80
|
+
[uri.path, uri.query].reject{ |str| str.nil?}.join("?")
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
metadata
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ey_snaplock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 8
|
10
|
+
version: 0.0.8
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Engine Yard
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-11-29 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: addressable
|
22
|
+
type: :runtime
|
23
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 2
|
32
|
+
- 2
|
33
|
+
version: 2.2.2
|
34
|
+
prerelease: false
|
35
|
+
requirement: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: SystemTimer
|
38
|
+
type: :runtime
|
39
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 29
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 2
|
48
|
+
- 1
|
49
|
+
version: 1.2.1
|
50
|
+
prerelease: false
|
51
|
+
requirement: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: rake
|
54
|
+
type: :development
|
55
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - "="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 49
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
- 8
|
64
|
+
- 7
|
65
|
+
version: 0.8.7
|
66
|
+
prerelease: false
|
67
|
+
requirement: *id003
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: realweb
|
70
|
+
type: :development
|
71
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 21
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
- 2
|
80
|
+
- 1
|
81
|
+
version: 0.2.1
|
82
|
+
prerelease: false
|
83
|
+
requirement: *id004
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: cucumber
|
86
|
+
type: :development
|
87
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ~>
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
hash: 55
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
- 10
|
96
|
+
- 0
|
97
|
+
version: 0.10.0
|
98
|
+
prerelease: false
|
99
|
+
requirement: *id005
|
100
|
+
- !ruby/object:Gem::Dependency
|
101
|
+
name: aruba
|
102
|
+
type: :development
|
103
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
105
|
+
requirements:
|
106
|
+
- - ~>
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
hash: 13
|
109
|
+
segments:
|
110
|
+
- 0
|
111
|
+
- 3
|
112
|
+
version: "0.3"
|
113
|
+
prerelease: false
|
114
|
+
requirement: *id006
|
115
|
+
- !ruby/object:Gem::Dependency
|
116
|
+
name: sinatra
|
117
|
+
type: :development
|
118
|
+
version_requirements: &id007 !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
hash: 3
|
124
|
+
segments:
|
125
|
+
- 0
|
126
|
+
version: "0"
|
127
|
+
prerelease: false
|
128
|
+
requirement: *id007
|
129
|
+
description: Server side components for Engine Yard's snapshotting process
|
130
|
+
email:
|
131
|
+
executables:
|
132
|
+
- ey-snaplock
|
133
|
+
extensions: []
|
134
|
+
|
135
|
+
extra_rdoc_files: []
|
136
|
+
|
137
|
+
files:
|
138
|
+
- bin/ey-snaplock
|
139
|
+
- lib/ey_snaplock/database/mysql.rb
|
140
|
+
- lib/ey_snaplock/database/postgresql9.rb
|
141
|
+
- lib/ey_snaplock/database.rb
|
142
|
+
- lib/ey_snaplock/version.rb
|
143
|
+
- lib/ey_snaplock.rb
|
144
|
+
- LICENSE
|
145
|
+
- README.md
|
146
|
+
homepage:
|
147
|
+
licenses: []
|
148
|
+
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
none: false
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
hash: 3
|
160
|
+
segments:
|
161
|
+
- 0
|
162
|
+
version: "0"
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
none: false
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
hash: 3
|
169
|
+
segments:
|
170
|
+
- 0
|
171
|
+
version: "0"
|
172
|
+
requirements: []
|
173
|
+
|
174
|
+
rubyforge_project:
|
175
|
+
rubygems_version: 1.8.6
|
176
|
+
signing_key:
|
177
|
+
specification_version: 3
|
178
|
+
summary: Server side components for Engine Yard's snapshotting process
|
179
|
+
test_files: []
|
180
|
+
|