methodmissing-scrooge 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/README.textile +262 -0
  2. data/lib/scrooge/core/string.rb +29 -0
  3. data/lib/scrooge/core/symbol.rb +21 -0
  4. data/lib/scrooge/core/thread.rb +26 -0
  5. data/lib/scrooge/framework/base.rb +304 -0
  6. data/lib/scrooge/framework/rails.rb +107 -0
  7. data/lib/scrooge/middleware/tracker.rb +47 -0
  8. data/lib/scrooge/orm/active_record.rb +143 -0
  9. data/lib/scrooge/orm/base.rb +102 -0
  10. data/lib/scrooge/profile.rb +204 -0
  11. data/lib/scrooge/storage/base.rb +47 -0
  12. data/lib/scrooge/storage/memory.rb +25 -0
  13. data/lib/scrooge/tracker/app.rb +101 -0
  14. data/lib/scrooge/tracker/base.rb +56 -0
  15. data/lib/scrooge/tracker/model.rb +90 -0
  16. data/lib/scrooge/tracker/resource.rb +201 -0
  17. data/lib/scrooge.rb +62 -0
  18. data/spec/fixtures/config/scrooge/scopes/1234567891/scope.yml +2 -0
  19. data/spec/fixtures/config/scrooge.yml +12 -0
  20. data/spec/helpers/framework/rails/cache.rb +25 -0
  21. data/spec/spec_helper.rb +51 -0
  22. data/spec/units/scrooge/core/string_spec.rb +21 -0
  23. data/spec/units/scrooge/core/symbol_spec.rb +13 -0
  24. data/spec/units/scrooge/core/thread_spec.rb +15 -0
  25. data/spec/units/scrooge/framework/base_spec.rb +154 -0
  26. data/spec/units/scrooge/framework/rails_spec.rb +40 -0
  27. data/spec/units/scrooge/orm/base_spec.rb +61 -0
  28. data/spec/units/scrooge/profile_spec.rb +73 -0
  29. data/spec/units/scrooge/storage/base_spec.rb +35 -0
  30. data/spec/units/scrooge/storage/memory_spec.rb +20 -0
  31. data/spec/units/scrooge/tracker/app_spec.rb +62 -0
  32. data/spec/units/scrooge/tracker/base_spec.rb +21 -0
  33. data/spec/units/scrooge/tracker/model_spec.rb +50 -0
  34. data/spec/units/scrooge/tracker/resource_spec.rb +83 -0
  35. data/spec/units/scrooge_spec.rb +13 -0
  36. metadata +110 -0
data/README.textile ADDED
@@ -0,0 +1,262 @@
1
+ h1. Scrooge
2
+
3
+ A Framework and ORM agnostic Model / record attribute tracker to ensure production
4
+ Ruby applications only fetch the database content needed to minimize wire traffic
5
+ and reduce conversion overheads to native Ruby types.
6
+
7
+ This is mostly an experiment into unobtrusive tracking, respecting development workflows
8
+ and understanding Rack internals better.
9
+
10
+ h2. Why bother ?
11
+
12
+ * Object conversion and moving unnecessary data is both expensive and tax existing infrastructure in high load setups
13
+ * Manually extracting and scoping SELECT clauses is not sustainable in a clean and painless manner with iterative development, even less so in large projects.
14
+
15
+ h2. Suggested Use
16
+
17
+ Scrooge *requires* a comprehensive existing functional or integration test suite for best results.If test coverage is flaky or non-existent, then a) you shouldn't worry about performance tuning and b) shouldn't be reading this - go spec.
18
+
19
+ There's 2 basic modes of operation, a tracking or a scope phase.
20
+
21
+ h4. Resources
22
+
23
+ A resource is :
24
+
25
+ * A controller and action endpoint ( inferred through framework specific routing )
26
+ * A content type / format - a PDF representation may have different Model attribute requirements than a vanilla ERB view.
27
+ * Request method - typically popular public facing GET requests
28
+
29
+ All Model to attribute mappings is tracked on a per Resource basis.Multiple Models per Resource is supported.
30
+
31
+ h4. Tracking
32
+
33
+ In tracking mode, which is the default when no scope is given and Scrooge is enabled, Scrooge installs filters ( either through Rack middleware or framework specific hooks ) that track attribute access on a per Resource basis.
34
+
35
+ A Kernel#at_exit callback dumps and timestamps this profile ( or scope ) to eg. *framework_configuration_directory/config/scopes/1234147851/scope.yml*
36
+
37
+ This typically happens during functional or integration testing.
38
+
39
+ The test log may look like :
40
+
41
+ <pre>
42
+ <code>
43
+ Processing HotelsController#index (for 0.0.0.0 at 2009-02-09 02:55:55) [GET]
44
+ Parameters: {"action"=>"index", "controller"=>"hotels"}
45
+ [Scrooge] Track with resource #< :/ ()
46
+ [Scrooge] Track for Resource #<GET :hotels/index (*/*)
47
+ - #<Hotel :from_price, :narrative, :star_rating, :latitude, :created_at, :hotel_name, :updated_at, :important_notes, :id, :apt, :location_id, :nearest_tube, :longitude, :telephone, :nearest_rail, :location_name, :distance>
48
+ - #<Image :thumbnail_width, :created_at, :title, :updated_at, :url, :thumbnail_height, :height, :thumbnail_url, :has_thumbnail, :hotel_id, :width>
49
+ Hotel Load (0.3ms) SELECT * FROM `hotels` LIMIT 0, 15
50
+ Rendering template within layouts/application
51
+ Rendering hotels/index
52
+ Image Load (0.2ms) SELECT * FROM `images` WHERE (`images`.hotel_id = 491) LIMIT 1
53
+ [Scrooge] read attribute updated_at
54
+ Rendered hotels/_hotel (2.7ms)
55
+ Rendered shared/_header (0.1ms)
56
+ Rendered shared/_navigation (0.3ms)
57
+ Missing template hotels/_index_sidebar.erb in view path app/views
58
+ Rendered shared/_sidebar (0.1ms)
59
+ Rendered shared/_footer (0.1ms)
60
+ Completed in 91ms (View: 90, DB: 1) | 200 OK [http://test.host/hotels]
61
+ SQL (0.3ms) ROLLBACK
62
+ SQL (0.1ms) BEGIN
63
+ </code>
64
+ </pre>
65
+
66
+ An example scope / profile, saved to disk :
67
+
68
+ <pre>
69
+ <code>
70
+ ---
71
+ - hotels_show_get:
72
+ :action: show
73
+ :controller: hotels
74
+ :method: :get
75
+ :format: "*/*"
76
+ :models:
77
+ - Address:
78
+ - line1
79
+ - line2
80
+ - created_at
81
+ - postcode
82
+ - updated_at
83
+ - country_id
84
+ - county
85
+ - location_id
86
+ - town
87
+ - hotel_id
88
+ - Hotel:
89
+ - important_notes
90
+ - location_id
91
+ - locations_index_get:
92
+ :action: index
93
+ :controller: locations
94
+ :method: :get
95
+ :format: "*/*"
96
+ :models:
97
+ - Location:
98
+ - name
99
+ - created_at
100
+ - code
101
+ - updated_at
102
+ - level
103
+ - id
104
+ - countries_index_get:
105
+ :action: index
106
+ :controller: countries
107
+ :method: :get
108
+ :format: "*/*"
109
+ :models:
110
+ - Country:
111
+ - name
112
+ - created_at
113
+ - code
114
+ - updated_at
115
+ - id
116
+ - location_id
117
+ - continent_id
118
+ - hotels_index_get:
119
+ :action: index
120
+ :controller: hotels
121
+ :method: :get
122
+ :format: "*/*"
123
+ :models:
124
+ - Hotel:
125
+ - from_price
126
+ - narrative
127
+ - star_rating
128
+ - latitude
129
+ - created_at
130
+ - hotel_name
131
+ - updated_at
132
+ - important_notes
133
+ - id
134
+ - apt
135
+ - location_id
136
+ - nearest_tube
137
+ - longitude
138
+ - telephone
139
+ - nearest_rail
140
+ - location_name
141
+ - distance
142
+ - Image:
143
+ - thumbnail_width
144
+ - created_at
145
+ - title
146
+ - updated_at
147
+ - url
148
+ - thumbnail_height
149
+ - height
150
+ - thumbnail_url
151
+ - has_thumbnail
152
+ - hotel_id
153
+ - width
154
+ </code>
155
+ </pre>
156
+
157
+ h4. Scoping
158
+
159
+ A previously persisted scope / profile can be restored from disk and injected to the applicable Resources.Database content retrieved will match that of the given scope timestamp.
160
+
161
+ This is typically pushed to production and adjusted for each major release or deployment.
162
+
163
+ Log output may look like :
164
+
165
+ <pre>
166
+ <code>
167
+ Processing HotelsController#index (for 0.0.0.0 at 2009-02-09 02:59:41) [GET]
168
+ Parameters: {"action"=>"index", "controller"=>"hotels"}
169
+ [Scrooge] Scope for Model #<Image :created_at, :thumbnail_width, :title, :updated_at, :url, :id, :thumbnail_height, :height, :thumbnail_url, :has_thumbnail, :width, :hotel_id>
170
+ [Scrooge] Scope for Model #<Hotel :narrative, :from_price, :created_at, :latitude, :star_rating, :hotel_name, :updated_at, :important_notes, :apt, :id, :nearest_tube, :location_id, :nearest_rail, :telephone, :longitude, :distance, :location_name>
171
+ Hotel Load (0.4ms) SELECT hotels.narrative, hotels.from_price, hotels.created_at, hotels.latitude, hotels.star_rating, hotels.hotel_name, hotels.updated_at, hotels.important_notes, hotels.apt, hotels.id, hotels.nearest_tube, hotels.location_id, hotels.nearest_rail, hotels.telephone, hotels.longitude, hotels.distance, hotels.location_name FROM `hotels` LIMIT 0, 15
172
+ Rendering template within layouts/application
173
+ Rendering hotels/index
174
+ Image Load (0.2ms) SELECT images.created_at, images.thumbnail_width, images.title, images.updated_at, images.url, images.id, images.thumbnail_height, images.height, images.thumbnail_url, images.has_thumbnail, images.width, images.hotel_id FROM `images` WHERE (`images`.hotel_id = 491) LIMIT 1
175
+ Rendered hotels/_hotel (2.8ms)
176
+ Rendered shared/_header (0.1ms)
177
+ Rendered shared/_navigation (0.3ms)
178
+ Missing template hotels/_index_sidebar.erb in view path app/views
179
+ Rendered shared/_sidebar (0.1ms)
180
+ Rendered shared/_footer (0.1ms)
181
+ Completed in 90ms (View: 5, DB: 1) | 200 OK [http://test.host/hotels]
182
+ SQL (0.1ms) ROLLBACK
183
+ SQL (0.1ms) BEGIN
184
+ </code>
185
+ </pre>
186
+
187
+ h2. Installation
188
+
189
+ h4. As a Rails plugin ( Recommended )
190
+
191
+ ./script/plugin install git://github.com/methodmissing/scrooge.git
192
+
193
+ h4. From Git
194
+
195
+ git pull git://github.com/methodmissing/scrooge.git
196
+
197
+
198
+ h4. As a Gem
199
+
200
+ sudo gem install methodmissing-scrooge -s http://gems.github.com
201
+
202
+ h2. Configuration
203
+
204
+ Scrooge installs ( see recommended installation above ) a configuration file with the following format within *framework_configuration_directory/scrooge.yml ( RAILS_ROOT/config/scrooge.yml for a Rails setup ) :
205
+
206
+ production:
207
+ orm: :active_record
208
+ storage: :memory
209
+ scope:
210
+ on_missing_attribute: :reload # or :raise
211
+ enabled: true
212
+ development:
213
+ orm: :active_record
214
+ storage: :memory
215
+ scope:
216
+ on_missing_attribute: :reload # or :raise
217
+ enabled: true
218
+ test:
219
+ orm: :active_record
220
+ storage: :memory
221
+ scope:
222
+ on_missing_attribute: :reload # or :raise
223
+ enabled: true
224
+
225
+ h4. ORM
226
+
227
+ Scrooge is ORM agnostic and ships with an ActiveRecord layer.
228
+
229
+ orm: :active_record
230
+
231
+ h4. Storage backend
232
+
233
+ Tracking results can be persisted to a given backend or storage option.Ships with a memory store, but can be extended to file system, memcached etc. as all Tracker components is designed to be Marshal friendly.
234
+
235
+ storage: :memory
236
+
237
+ h4. Scope
238
+
239
+ A scope is a reference to a timestamped Scrooge run where access to Model attributes is tracked on a per Resource basis.
240
+
241
+ scope: 1234567891
242
+
243
+ If not scope is given in the configuration, ENV['scope'] would also be considered to facilitate configuration through Capistrano etc.
244
+
245
+ h4. Handling Missing Attributes
246
+
247
+ When the contents for a given Model attribute has not been retrieved from the database, most ORM frameworks raise an error by default.This is configurable to reloading the model with all it's columns or raise instead.
248
+
249
+ on_missing_attribute: :reload # or :raise
250
+
251
+ h4. Status
252
+
253
+ Scrooge can be disabled with :
254
+
255
+ enabled: false
256
+
257
+ h2. Notes
258
+
259
+ This is an initial release, has not yet been battle tested in production and is pending Ruby 1.9.1 compatibility.
260
+
261
+ Initially evaluated a centralized tracker concept for multi-server environments with minimal configuration overhead
262
+ and on the fly scope injection after a given warmup threshold but found that to be overkill for a first release.
@@ -0,0 +1,29 @@
1
+ module Scrooge
2
+ module Core
3
+ module String
4
+
5
+ # Framework agnostic String <=> Constant helpers.
6
+ # Perhaps not the cleanest abstraction, but also not good practice to piggy
7
+ # back on or use a naming convention that may clash with and uproot the API
8
+ # any given framework ships with.
9
+
10
+ def to_const
11
+ self.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
12
+ end
13
+
14
+ def to_const!( instantiate = true )
15
+ begin
16
+ const = Object.module_eval(to_const, __FILE__, __LINE__)
17
+ instantiate ? const.new : const
18
+ rescue => exception
19
+ exception.to_s.match( /uninitialized constant/ ) ? self : raise
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+
27
+ class String
28
+ include Scrooge::Core::String
29
+ end
@@ -0,0 +1,21 @@
1
+ module Scrooge
2
+ module Core
3
+ module Symbol
4
+
5
+ # See Scrooge::Core::Symbol
6
+
7
+ def to_const
8
+ to_s.to_const
9
+ end
10
+
11
+ def to_const!
12
+ to_s.to_const!
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+
19
+ class Symbol
20
+ include Scrooge::Core::Symbol
21
+ end
@@ -0,0 +1,26 @@
1
+ module Scrooge
2
+ module Core
3
+ module Thread
4
+
5
+ # Scrooge Resource tracker scoped to the current Thread for threadsafety in
6
+ # multi-threaded environments.
7
+
8
+ def scrooge_resource
9
+ current[:scrooge_resource] ||= Scrooge::Tracker::Resource.new
10
+ end
11
+
12
+ def scrooge_resource=( resource )
13
+ current[:scrooge_resource] = resource
14
+ end
15
+
16
+ def reset_scrooge_resource!
17
+ current[:scrooge_resource] = nil
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+
24
+ class Thread
25
+ extend Scrooge::Core::Thread
26
+ end
@@ -0,0 +1,304 @@
1
+ module Scrooge
2
+ module Framework
3
+
4
+ # Scrooge is framework agnostic and attempts to abstract the following :
5
+ #
6
+ # * current environment
7
+ # * app root dir
8
+ # * app tmp dir
9
+ # * app config dir
10
+ # * logging
11
+ # * resource endpoints
12
+ # * caching
13
+ # * injecting Rack MiddleWare
14
+ #
15
+ # Framework Signatures
16
+ #
17
+ # Scrooge will attempt to determine the current active framework it's deployed with
18
+ # through various framework specific hooks.
19
+ #
20
+ # module Scrooge
21
+ # module Framework
22
+ # module YetAnother < Base
23
+ # ...
24
+ # signature do
25
+ # Object.const_defined?( "UnlikeAnyOther" )
26
+ # end
27
+ # ...
28
+ # end
29
+ # end
30
+ # end
31
+
32
+ autoload :Rails, 'scrooge/framework/rails'
33
+
34
+ class Base < Scrooge::Base
35
+
36
+ GUARD = Mutex.new
37
+
38
+ SCOPE_REGEX = /\d{10}/.freeze
39
+
40
+ CONFIGURATION_FILE = 'scrooge.yml'.freeze
41
+
42
+ SCOPE_FILE = 'scope.yml'.freeze
43
+
44
+ class NotImplemented < StandardError
45
+ end
46
+
47
+ class NoSupportedFrameworks < StandardError
48
+ end
49
+
50
+ class InvalidScopeSignature < StandardError
51
+ end
52
+
53
+ class << self
54
+
55
+ # Per framework signature lookup.
56
+ #
57
+ @@signatures = {}
58
+ @@signatures[self.name] = Hash.new( [] )
59
+
60
+ # Support none by default.
61
+ #
62
+ @@frameworks = []
63
+
64
+ # Registers a framework signature.
65
+ #
66
+ def signature( &block )
67
+ @@signatures[self.name] = signatures << block
68
+ end
69
+
70
+ # All signatures for the current klass.
71
+ #
72
+ def signatures
73
+ @@signatures[self.name] || []
74
+ end
75
+
76
+ # All supported frameworks.
77
+ #
78
+ def frameworks
79
+ @@frameworks
80
+ end
81
+
82
+ # Infer the framework Scrooge attaches to in a first yield manner.
83
+ # A match of all defined signatures is required.
84
+ #
85
+ def which_framework?
86
+ iterate_frameworks() || raise( NoSupportedFrameworks )
87
+ end
88
+
89
+ # Yield an instance of the current framework.
90
+ #
91
+ def instantiate
92
+ which_framework?().new
93
+ end
94
+
95
+ private
96
+
97
+ def inherited( subclass ) #:nodoc:
98
+ @@frameworks << subclass
99
+ end
100
+
101
+ def iterate_frameworks #:nodoc:
102
+ frameworks.detect do |framework|
103
+ framework.signatures.all?{|sig| sig.call }
104
+ end
105
+ end
106
+
107
+ end
108
+
109
+ # The framework environment eg. test, development etc.
110
+ #
111
+ def environment
112
+ raise NotImplemented
113
+ end
114
+
115
+ # Application root directory
116
+ #
117
+ def root
118
+ raise NotImplemented
119
+ end
120
+
121
+ # Application temp. directory
122
+ #
123
+ def tmp
124
+ raise NotImplemented
125
+ end
126
+
127
+ # Application configuration directory
128
+ #
129
+ def config
130
+ raise NotImplemented
131
+ end
132
+
133
+ # Application logger instance.
134
+ # API compat with stdlib Logger assumed.
135
+ #
136
+ def logger
137
+ raise NotImplemented
138
+ end
139
+
140
+ # Supplement the current Resource tracker with additional environment context.
141
+ #
142
+ def resource( env, request = nil )
143
+ raise NotImplemented
144
+ end
145
+
146
+ # Write to the framework cache.
147
+ #
148
+ def write_cache( key, value )
149
+ raise NotImplemented
150
+ end
151
+
152
+ # Read from the framework cache.
153
+ #
154
+ def read_cache( key )
155
+ raise NotImplemented
156
+ end
157
+
158
+ # Access to the framework's Rack middleware stack.
159
+ #
160
+ def middleware
161
+ raise NotImplemented
162
+ end
163
+
164
+ # Inject scoping middleware.
165
+ #
166
+ def install_scope_middleware( tracker )
167
+ raise NotImplemented
168
+ end
169
+
170
+ # Inject tracking middleware.
171
+ #
172
+ def install_tracking_middleware
173
+ raise NotImplemented
174
+ end
175
+
176
+ # Register a code block to run when the host framework is fully initialized.
177
+ #
178
+ def initialized( &block )
179
+ raise NotImplemented
180
+ end
181
+
182
+ # Yields a controller constant from a given Resource Tracker
183
+ #
184
+ def controller( resource )
185
+ raise NotImplemented
186
+ end
187
+
188
+ # Retrieve all previously persisted scopes tracked with Scrooge.
189
+ #
190
+ def scopes
191
+ ensure_scopes_path do
192
+ Dir.entries( scopes_path ).grep( SCOPE_REGEX )
193
+ end
194
+ end
195
+
196
+ # Determine if there's any previously persisted scopes.
197
+ #
198
+ def scopes?
199
+ !scopes().empty?
200
+ end
201
+
202
+ # Return the scopes storage path for the current framework.
203
+ #
204
+ def scopes_path
205
+ @profiles_path ||= File.join( config, 'scrooge', 'scopes' )
206
+ end
207
+
208
+ # Return the scopes storage path for a given scope and optional filename.
209
+ #
210
+ def scope_path( scope, filename = nil )
211
+ path = File.join( scopes_path, scope.to_s )
212
+ filename ? File.join( path, filename ) : path
213
+ end
214
+
215
+ # Log a message to the logger.
216
+ #
217
+ def log( message )
218
+ logger.info "[Scrooge] #{message}"
219
+ end
220
+
221
+ # Persist the current tracker as scope or restore a previously persisted scope
222
+ # from a given signature.
223
+ #
224
+ def scope!( scope = nil )
225
+ scope ? from_scope!( scope ) : to_scope!()
226
+ end
227
+
228
+ # Do we have a valid scope signature ?
229
+ #
230
+ def scope?( scope )
231
+ scopes.include?( scope.to_s )
232
+ end
233
+
234
+ # Restore a previously persisted scope to the current tracker from a given
235
+ # signature.Raises Scrooge::Framework::InvalidScopeSignature if the signature
236
+ # could not be found.
237
+ #
238
+ def from_scope!( scope )
239
+ GUARD.synchronize do
240
+ if scope?( scope )
241
+ restore_scope!( scope )
242
+ else
243
+ raise InvalidScopeSignature
244
+ end
245
+ end
246
+ end
247
+
248
+ # Dump the current tracker to the filesystem.
249
+ #
250
+ def to_scope!
251
+ GUARD.synchronize do
252
+ scope = Time.now.to_i
253
+ dump_scope!( scope )
254
+ scope
255
+ end
256
+ end
257
+
258
+ # Full path the scrooge configuration file.
259
+ #
260
+ def configuration_file
261
+ @configuration_file ||= File.join( config, CONFIGURATION_FILE )
262
+ end
263
+
264
+ private
265
+
266
+ def restore_scope!( scope ) #:nodoc:
267
+ tracker = Scrooge::Tracker::App.new
268
+ tracker.marshal_load( scope_from_yaml( scope ) )
269
+ tracker
270
+ end
271
+
272
+ def dump_scope!( scope ) #:nodoc:
273
+ ensure_scope_path( scope ) do
274
+ File.open( scope_path( scope, SCOPE_FILE ), 'w' ) do |io|
275
+ scope_to_yaml( io )
276
+ end
277
+ end
278
+ end
279
+
280
+ def scope_from_yaml( scope ) #:nodoc:
281
+ YAML.load( IO.read( scope_path( scope.to_s, SCOPE_FILE ) ) )
282
+ end
283
+
284
+ def scope_to_yaml( io ) #:nodoc:
285
+ YAML.dump( Scrooge::Base.profile.tracker.marshal_dump, io )
286
+ end
287
+
288
+ def ensure_scope_path( scope ) #:nodoc:
289
+ makedir_unless_exist( scope_path( scope ) )
290
+ yield if block_given?
291
+ end
292
+
293
+ def ensure_scopes_path #:nodoc:
294
+ makedir_unless_exist( scopes_path )
295
+ yield if block_given?
296
+ end
297
+
298
+ def makedir_unless_exist( path ) #:nodoc:
299
+ FileUtils.makedirs( path ) unless File.exist?( path )
300
+ end
301
+
302
+ end
303
+ end
304
+ end