zd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.md +220 -0
  2. data/lib/zd.rb +159 -0
  3. data/lib/zd/version.rb +3 -0
  4. metadata +62 -0
@@ -0,0 +1,220 @@
1
+ # ZD
2
+
3
+ ZD is a zero-downtime data migration framework that sits on top of the data store (or stores) of your choice. It implements zero-downtime by putting your data through a series of states:
4
+
5
+ 1. **Unrun**:
6
+ The migration is implemented, but has not been run yet.
7
+ 2. **Prepared**:
8
+ Any necessary creation of a "space" for the migrated data has been done. Typically not needed for schema-less stores. Your application code now writes to both the old and new data structures.
9
+ 3. **Migrated**:
10
+ All pre-existing data has been copied and/or mutated in to the new locations while continuing to exist as-is in the old locations. The code base is still operating off of the old locations (and new data continues to be written to both locations).
11
+ 4. **Switched**:
12
+ The code base now uses the new locations and ignores the old locations. Data is still written to both old and new locations. This is the point at which you would want to test the system to make sure the migration worked as expected. If something isn't right, the migration can still be rolled all the way back to the `unrun` state.
13
+ 5. **Completed**:
14
+ The migration has been verified to be working, and new data is no longer written to the old locations. Migration-specific code can be stripped out of the code base now. Once a migration is `completed`, it cannot be rolled back. Any references to the migration in the codebase will generate a warning.
15
+ 6. **Destroyed**:
16
+ The old locations for data have been removed from the data store. Any references to the migration in the codebase will raise an error.
17
+
18
+ ## Installation
19
+
20
+ Just add zd to your Gemfile:
21
+
22
+ gem 'zd'
23
+
24
+ And `bundle install`.
25
+
26
+ ## Usage
27
+
28
+ To show how ZD works, lets walk through a simple example. Lets say you have a Person class, which used to store separate first names and last names. You've since expanded internationally and realized what a bad idea this is in general, and so now you need to fix your mistake down to the data level *without* taking your application down (though rolling restarts are OK). Your Person class looks like this initially:
29
+
30
+ class Person
31
+ include AwesomeDB
32
+
33
+ def first_name
34
+ read(:first_name)
35
+ end
36
+
37
+ def last_name
38
+ read(:last_name)
39
+ end
40
+
41
+ def first_name=(value)
42
+ write(:first_name, value)
43
+ end
44
+
45
+ def last_name=(value)
46
+ write(:last_name, value)
47
+ end
48
+
49
+ def name
50
+ [first_name, last_name].compact.join(" ")
51
+ end
52
+
53
+ def name=(value)
54
+ first_name, *rest = value.split(/\s+/)
55
+ write(:first_name, first_name)
56
+ write(:last_name, rest.join(" "))
57
+ end
58
+ end
59
+
60
+ (The `read` and `write` methods are made-up access methods for the made-up AwesomeDB data store.)
61
+
62
+ Currently you still have code using both the `name` and `first_name`/`last_name`, but you're slowly cleaning it up. The key thing is that all the methods on the class continue to obey their contract throughout the data migration.
63
+
64
+ To get started you'll want to generate a new migration with `zd new <name>`. Migrations go in the db/migrate folder in your project, and use a timestamped filename (similar to ActiveRecord migrations). A fresh migration looks something like this:
65
+
66
+ class Migrations::MergeFirstAndLastName < ZD::Migration
67
+ register! depends_on: :nothing
68
+
69
+ def prepare
70
+ end
71
+
72
+ def migrate
73
+ end
74
+
75
+ def destroy
76
+ end
77
+ end
78
+
79
+
80
+ And here is what it might look like after the migration is filled out:
81
+
82
+ class Migrations::MergeFirstAndLastName < ZD::Migration
83
+ register! depends_on: :nothing
84
+
85
+ def prepare
86
+ Person.add_field :name
87
+ end
88
+
89
+ def migrate
90
+ Person.each do |person|
91
+ person.name = [person.first_name, person.last_name].compact.join(" ")
92
+ end
93
+ end
94
+
95
+ def destroy
96
+ Person.remove_field :first_name
97
+ Person.remove_field :last_name
98
+ end
99
+ end
100
+
101
+
102
+ The `Person.add_field` and `Person.remove_field` methods are made up; you would just use whatever your data store provides (if necessary; many schemaless datastores won't even need the `prepare` step).
103
+
104
+ This is all well and good, but how does the model handle the fact that the data format is shifting around underneath it? ZD provides state-based methods that can be used to mark when which code should be run:
105
+
106
+ class Person
107
+ include AwesomeDB
108
+
109
+ def first_name
110
+ ZD[:merge_first_and_last_name].HANDLE do |m|
111
+ m.UNTIL_SWITCHED{read(:first_name)}
112
+ m.ONCE_SWITCHED{@first_name ||= name.split(/\s+/).first}
113
+ end
114
+ end
115
+
116
+ def last_name
117
+ ZD[:merge_first_and_last_name].HANDLE do |m|
118
+ m.UNTIL_SWITCHED{read(:last_name)}
119
+ m.ONCE_SWITCHED{@last_name ||= name.split(/\s+/)[1..-1].join(" ")}
120
+ end
121
+ end
122
+
123
+ def first_name=(value)
124
+ ZD[:merge_first_and_last_name].HANDLE do |m|
125
+ m.ONCE_PREPARED{write(:name, [value, last_name].compact.join(" "))}
126
+ m.UNTIL_COMPLETED{write(:first_name, value)}
127
+ end
128
+ end
129
+
130
+ def last_name=(value)
131
+ ZD[:merge_first_and_last_name].HANDLE do |m|
132
+ m.ONCE_PREPARED{write(:name, [first_name, value].compact.join(" "))}
133
+ m.UNTIL_COMPLETED{write(:last_name, value)}
134
+ end
135
+ end
136
+
137
+ def name
138
+ ZD[:merge_first_and_last_name].HANDLE do |m|
139
+ m.UNTIL_SWITCHED{return [first_name, last_name].compact.join(" ")}
140
+ m.ONCE_SWITCHED{read(:name)}
141
+ end
142
+ end
143
+
144
+ def name=(value)
145
+ ZD[:merge_first_and_last_name].HANDLE do |m|
146
+ m.ONCE_PREPARED{write(:name, value)}
147
+ m.UNTIL_COMPLETED do
148
+ first_name, *rest = value.split(/\s+/)
149
+ write(:first_name, first_name)
150
+ write(:last_name, rest.join(" "))
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+
157
+ The first thing you're probably thinking after seeing that is, "Who hit my code with the ugly stick!?!" But that's actually a feature of ZD: migration-specific code sticks out like a sore thumb so that there will be lots of motivation to strip it out once the migration is complete. Migration code should be robust but temporary.
158
+
159
+ Once your migration and migration-specific code is in place, you can start walking your data through the migration states using `zd`:
160
+
161
+ $ zd prepare
162
+
163
+ All migrations in the `unrun` state will be transitioned to the `prepared` state via the `prepare` action.
164
+
165
+ $ zd migrate
166
+
167
+ All migrations in the `prepared` state will be transitioned to the `migrated` state via the `migrate` action.
168
+
169
+ $ zd switch
170
+
171
+ All migrations in the `migrated` state will flip over to `switched`. This triggers all code to start using the new code paths.
172
+
173
+ This is the point at which you should verify that your migrations have been successful and all the new code is working as expected in production. Getting back to the old state is as easy as `zd switchoff [name]`.
174
+
175
+ $ zd complete
176
+
177
+ All migrations in the `switched` state will flip over to `completed`. This triggers all code to stop writing to old locations, and puts you past the point of no return for an easy rollback. Once you get here, it's time to go through your codebase and rip out the migration-specific code blocks, just leaving the code that deals with the new data structure.
178
+
179
+ $ zd destroy
180
+
181
+ All migrations in the `completed` state will be transitioned to the `destroyed` state via the `destroy` action. Typically this is the point at which old data gets cleaned up. Note that once your migration gets to this state, continued references to it in your code will raise an error.
182
+
183
+ And that's all there is to it! You can either leave old migration files from db/migrate, or delete them once you're done with them - the overhead for each one is very small. Oh, and here's what the Person class looks like once you're done:
184
+
185
+ class Person
186
+ include AwesomeDB
187
+
188
+ def first_name
189
+ @first_name ||= name.split(/\s+/).first
190
+ end
191
+
192
+ def last_name
193
+ @last_name ||= name.split(/\s+/)[1..-1].join(" ")
194
+ end
195
+
196
+ def first_name=(value)
197
+ write(:name, [value, last_name].compact.join(" "))
198
+ end
199
+
200
+ def last_name=(value)
201
+ write(:name, [first_name, value].compact.join(" "))
202
+ end
203
+
204
+ def name
205
+ read(:name)
206
+ end
207
+
208
+ def name=(value)
209
+ write(:name, value)
210
+ end
211
+ end
212
+
213
+ No more ugly!
214
+
215
+
216
+ ## State Tracking
217
+
218
+ ## Dependencies
219
+
220
+ ## Philosophy
@@ -0,0 +1,159 @@
1
+ require "active_support/core_ext/string"
2
+
3
+ module ZD
4
+ def self.[](name)
5
+ migrations[name]
6
+ end
7
+
8
+ def HANDLE(name)
9
+ yield(self[name])
10
+ end
11
+
12
+ def self.load!(path)
13
+ Dir["#{path}/*.rb"].each do |e|
14
+ if block_given?
15
+ yield(e)
16
+ else
17
+ require e
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.migrations
23
+ @migrations ||= {}
24
+ end
25
+
26
+ def self.register_migration(name, migration, dependencies)
27
+ if dependencies.empty?
28
+ raise ArgumentError, "You must declare migration dependencies (or pass :nothing)"
29
+ end
30
+ migrations[name] = migration
31
+ dependencies.each do |d|
32
+ add_dependency(name, d)
33
+ end
34
+ end
35
+
36
+ def self.add_dependency(migration, depends_on)
37
+ raise ArgumentError, "Dependency #{depends_on} not found for #{migration}" unless(depends_on == :nothing || self[depends_on])
38
+ dependencies[depends_on] << migration
39
+ end
40
+
41
+ def self.dependencies
42
+ @dependencies ||= Hash.new{|h,k| h[k] = []}
43
+ end
44
+
45
+ class Migration
46
+ def self.register!(options={})
47
+ migration_name = (options[:name] || name.split(/::/).last.underscore.to_sym)
48
+ initial_state = (options[:initial_state] || :destroyed)
49
+ initial_state = initial_state.call if initial_state.respond_to?(:call)
50
+ ZD.register_migration(migration_name, new(initial_state), Array(options[:depends_on]))
51
+ end
52
+
53
+ STATES = [:unrun, :prepared, :migrated, :switched, :completed, :destroyed]
54
+ TRANSITIONS = {
55
+ unrun: :prepare,
56
+ prepared: :migrate,
57
+ migrated: :switch,
58
+ switched: :complete,
59
+ completed: :destroy,
60
+ }
61
+
62
+ attr_reader :state
63
+ def initialize(initial_state)
64
+ self.state = initial_state
65
+ end
66
+
67
+ def state=(new_state)
68
+ check_state(new_state)
69
+ @state = new_state
70
+ end
71
+
72
+ def check_state(state)
73
+ raise ArgumentError, "Unknown state #{state}" unless STATES.include?(state)
74
+ end
75
+
76
+ def migrate_to(new_state, silent=false)
77
+ @silent = silent
78
+ check_state(new_state)
79
+ currently = STATES.index(state)
80
+ target = STATES.index(new_state)
81
+ to_run = STATES[(currently)..(target-1)]
82
+ to_run.each do |s|
83
+ action_name = TRANSITIONS[s]
84
+ puts "Transitioning from #{s} via #{action_name}..."
85
+ send(action_name) if action_name && respond_to?(action_name)
86
+ self.state = s
87
+ end
88
+ end
89
+
90
+ def already?(in_state)
91
+ STATES.index(@state) >= STATES.index(in_state)
92
+ end
93
+
94
+ def HANDLE
95
+ yield self
96
+ end
97
+
98
+ def UNTIL_PREPARED
99
+ unless already?(:prepared)
100
+ yield
101
+ end
102
+ end
103
+
104
+ def ONCE_PREPARED
105
+ if already?(:prepared)
106
+ yield
107
+ end
108
+ end
109
+
110
+ def UNTIL_SWITCHED
111
+ unless already?(:switched)
112
+ yield
113
+ end
114
+ end
115
+
116
+ def ONCE_SWITCHED
117
+ if already?(:switched)
118
+ yield
119
+ end
120
+ end
121
+
122
+ def UNTIL_COMPLETED
123
+ unless already?(:completed)
124
+ yield
125
+ end
126
+ end
127
+
128
+ def ONCE_COMPLETED
129
+ if already?(:completed)
130
+ yield
131
+ end
132
+ end
133
+
134
+ def ONCE_DESTROYED
135
+ if already?(:destroyed)
136
+ yield
137
+ end
138
+ end
139
+
140
+ def TEST_ONCE_SWITCHED
141
+ old_state = state
142
+ migrate_to(:switched, true)
143
+ yield
144
+ ensure
145
+ self.state = old_state
146
+ end
147
+
148
+ def puts(v="\n")
149
+ super(v) unless @silent
150
+ end
151
+
152
+ def print(v)
153
+ super(v) unless @silent
154
+ end
155
+
156
+ end
157
+ end
158
+
159
+ module Migrations; end
@@ -0,0 +1,3 @@
1
+ module ZD
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nathaniel Talbott
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-23 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &70132156417960 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70132156417960
25
+ description: ZD implements zero-downtime migrations as a managed series of state changes,
26
+ with hooks for you to manage your code and data throughout the migration.
27
+ email:
28
+ - nathaniel@talbott.ws
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - lib/zd.rb
35
+ - lib/zd/version.rb
36
+ homepage: http://github.com/ntalbott/zd
37
+ licenses: []
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: 1.9.2
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 1.5.2
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 1.8.10
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: A zero-downtime data migration framework that sits on top of the data store
60
+ (or stores) of your choice.
61
+ test_files: []
62
+ has_rdoc: