ey_snaplock 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
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
+ ![EY Snaplock](http://img825.imageshack.us/img825/4323/snaplock.jpg "EY Snaplock")
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,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require 'ey_snaplock'
5
+
6
+ EY::Snaplock.call(*ARGV)
@@ -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
@@ -0,0 +1,5 @@
1
+ module EY
2
+ class Snaplock
3
+ VERSION = '0.0.8'
4
+ end
5
+ end
@@ -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
+