fastaccess 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +137 -0
  3. data/Rakefile +38 -0
  4. data/lib/fastaccess.rb +13 -0
  5. data/lib/fastaccess/acts_with_fastaccess_on.rb +54 -0
  6. data/lib/fastaccess/fastaccess.rb +204 -0
  7. data/lib/fastaccess/mixins.rb +13 -0
  8. data/lib/fastaccess/version.rb +4 -0
  9. data/lib/generators/fastaccess/initialize_generator.rb +12 -0
  10. data/lib/generators/fastaccess/templates/initializer.rb +3 -0
  11. data/lib/tasks/fastaccess_tasks.rake +4 -0
  12. data/test/dummy/README.rdoc +261 -0
  13. data/test/dummy/Rakefile +7 -0
  14. data/test/dummy/app/assets/javascripts/application.js +15 -0
  15. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  16. data/test/dummy/app/controllers/application_controller.rb +3 -0
  17. data/test/dummy/app/helpers/application_helper.rb +2 -0
  18. data/test/dummy/app/models/simple_string.rb +42 -0
  19. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  20. data/test/dummy/config.ru +4 -0
  21. data/test/dummy/config/application.rb +59 -0
  22. data/test/dummy/config/boot.rb +10 -0
  23. data/test/dummy/config/database.yml +25 -0
  24. data/test/dummy/config/environment.rb +5 -0
  25. data/test/dummy/config/environments/development.rb +37 -0
  26. data/test/dummy/config/environments/production.rb +67 -0
  27. data/test/dummy/config/environments/test.rb +37 -0
  28. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  29. data/test/dummy/config/initializers/fastaccess.rb +3 -0
  30. data/test/dummy/config/initializers/inflections.rb +15 -0
  31. data/test/dummy/config/initializers/mime_types.rb +5 -0
  32. data/test/dummy/config/initializers/redis.rb +1 -0
  33. data/test/dummy/config/initializers/secret_token.rb +7 -0
  34. data/test/dummy/config/initializers/session_store.rb +8 -0
  35. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  36. data/test/dummy/config/locales/en.yml +5 -0
  37. data/test/dummy/config/routes.rb +58 -0
  38. data/test/dummy/db/development.sqlite3 +0 -0
  39. data/test/dummy/db/migrate/20130123183844_create_simple_strings.rb +8 -0
  40. data/test/dummy/db/migrate/20130124180635_add_some_string_to_simple_string.rb +5 -0
  41. data/test/dummy/db/schema.rb +22 -0
  42. data/test/dummy/db/test.sqlite3 +0 -0
  43. data/test/dummy/log/development.log +58 -0
  44. data/test/dummy/log/test.log +1575 -0
  45. data/test/dummy/public/404.html +26 -0
  46. data/test/dummy/public/422.html +26 -0
  47. data/test/dummy/public/500.html +25 -0
  48. data/test/dummy/public/favicon.ico +0 -0
  49. data/test/dummy/script/rails +6 -0
  50. data/test/dummy/test/fixtures/simple_strings.yml +11 -0
  51. data/test/dummy/test/unit/simple_string_test.rb +7 -0
  52. data/test/fastaccess_test.rb +76 -0
  53. data/test/test_helper.rb +15 -0
  54. metadata +174 -0
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
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.
@@ -0,0 +1,137 @@
1
+ # fastaccess - redisfast access on generated content
2
+
3
+ Many web applications which provide textual content to the user
4
+ have much more reads than writes. This can mean, that on
5
+ every 10k reads there is one write (see some popular
6
+ blogs or newspages).
7
+
8
+ Often this content is generated, because the input source
9
+ is some markup format, like markdown. This means, that
10
+ there is always the need to generate this content into
11
+ html.
12
+
13
+ And this is why fastaccess exists.
14
+ It modifies any given method, which generates content,
15
+ and stores it in a [redis][1] database.
16
+ If this content solely depends on a model attribute,
17
+ for example like the *body* in a blog post, it will
18
+ be auto updated, if the underlying model attribute
19
+ changes. Otherwise you can trigger the update manually.
20
+
21
+ Now lets see how this works:
22
+
23
+ ## Using fastaccess
24
+
25
+ First, of course, you'll need to include this gem in your
26
+ projects *Gemfile*.
27
+
28
+ Since this gem utilizes redis, make sure that this is installed
29
+ and generate the default initializer with
30
+
31
+ rails generate fastaccess:initialize
32
+
33
+ This will create a file in *config/initializers/fastaccess.rb* which
34
+ will create an instance of a redis database-connection. If you already
35
+ have one of those, feel free to replace the new instance with your version.
36
+
37
+ Now, in your project you will have to use the gem-provided
38
+ `acts_with_fastaccess_on` method to mark certain methods
39
+ as registered with fastaccess for caching.
40
+
41
+ ```ruby
42
+ class Post < ActiveRecord::Base
43
+ attr_accessible :title, :body
44
+ acts_with_fastaccess_on :markdown_body
45
+
46
+ def markdown_body
47
+ markdown(self.body)
48
+ end
49
+ end
50
+ ```
51
+
52
+ In this example a Model named *Post* is defined which has title and
53
+ body attributes. The `markdown_body` method converts the markdown-body to
54
+ html. Since this is needed often, it cached via redis.
55
+
56
+ The previous example utilizes a method, which operates on one of the models
57
+ attributes. So the redis-content will be updated if the pertaining Model
58
+ instance is updated. But what if you have a method which uses more input
59
+ than just specific attributes, which will update the models timestamp?
60
+
61
+ For this case there is an explicit update method, which allows you
62
+ to trigger an update.
63
+
64
+ ```ruby
65
+ # app/models/post.rb
66
+ class Post < ActiveRecord::Base
67
+ attr_accessible :title, :body, :tags
68
+ has_many :tags
69
+ acts_with_fastaccess_on :tag_list
70
+ include Fastaccess::Mixins
71
+
72
+ def tag_list
73
+ self.tags.map(&name)
74
+ end
75
+ end
76
+
77
+ # app/controllers/posts_controller.rb
78
+ class PostsController < ApplicationController
79
+ def update
80
+ @post = Post.find_by_id(params[:id])
81
+ @post.update_on :tag_list
82
+ respond_to do |format|
83
+ if @post.update_attributes(params[:post])
84
+ format.html { redirect_to @post, notice: 'Post was successfully updated.' }
85
+ format.json { head :no_content }
86
+ else
87
+ format.html { render action: "edit" }
88
+ format.json { render json: @post.errors, status: :unprocessable_entity }
89
+ end
90
+ end
91
+ end
92
+ ```
93
+
94
+ If you don't want to *pollute* your model with these mixin-methods you
95
+ can also call
96
+
97
+ ```ruby
98
+ Fastaccess::Fastaccess.update_content @post, :on => :tag_list, :arguments => []
99
+ ```
100
+
101
+ ## Features
102
+
103
+ ### planned features
104
+
105
+ - ~~disable auto-update via option setting (planned for *0.0.2*)~~ *implemented*
106
+ - more update flexibility
107
+ - e.g. custom update-constraints instead of calling `update_content` manually
108
+
109
+ ## License
110
+
111
+ ([The MIT License][mit])
112
+
113
+ Copyright © 2013:
114
+
115
+ - [Tim Reddehase][1]
116
+
117
+ Permission is hereby granted, free of charge, to any person obtaining
118
+ a copy of this software and associated documentation files (the
119
+ 'Software'), to deal in the Software without restriction, including
120
+ without limitation the rights to use, copy, modify, merge, publish,
121
+ distribute, sublicense, and/or sell copies of the Software, and to
122
+ permit persons to whom the Software is furnished to do so, subject to
123
+ the following conditions:
124
+
125
+ The above copyright notice and this permission notice shall be
126
+ included in all copies or substantial portions of the Software.
127
+
128
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
129
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
130
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
131
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
132
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
133
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
134
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
135
+
136
+ [mit]: http://opensource.org/licenses/MIT
137
+ [1]: http://rightsrestricted.com
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'Fastaccess'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,13 @@
1
+ require 'redis'
2
+ require 'fastaccess/fastaccess.rb'
3
+ require 'fastaccess/mixins.rb'
4
+ require 'fastaccess/acts_with_fastaccess_on'
5
+
6
+ module Fastaccess
7
+
8
+ # convenience method to setup fastaccess
9
+ def self.setup(&block)
10
+ Fastaccess.setup &block
11
+ end
12
+
13
+ end
@@ -0,0 +1,54 @@
1
+ module Fastaccess
2
+ module ActsWithFastaccessOn
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ end
7
+
8
+ module ClassMethods
9
+ # registers the pertaining method as one, which return value
10
+ # is provided through redis.
11
+ # This is for fast access of otherwise generated content
12
+ # with a high reading/writing ratio.
13
+ #
14
+ # @param [Symbol] method_name denoting the pertaining method
15
+ # this method shouldn't be defined beforehand.
16
+ # @param [Hash] options is basic options hash.
17
+ # currently has no effect on execution.
18
+ def acts_with_fastaccess_on(method_name, options = {})
19
+ Fastaccess.register_on self, method_name, options
20
+ # options = Fastaccess.merge_defaults(options)
21
+ define_singleton_method :method_added do |on_method|
22
+ if Fastaccess.registered? self, on_method
23
+ method = on_method
24
+ alias_name = Fastaccess.alias_for method
25
+ if !method_defined?(alias_name)
26
+ alias_method alias_name, method
27
+ define_method method do |*args|
28
+ fastaccess_id = Fastaccess.id_for(self)
29
+ redis_id = "#{method}_#{fastaccess_id}"
30
+ opts = Fastaccess.options_for(self, method)
31
+ content_current = opts[:auto_update] ? Fastaccess.update_check(self) : true
32
+ if Fastaccess.redis.exists(redis_id) && content_current
33
+ response = Fastaccess.redis.get(redis_id)
34
+ begin
35
+ return JSON.parse response
36
+ rescue JSON::ParserError
37
+ return response
38
+ end
39
+ else
40
+ response = method(alias_name).call(*args)
41
+ Fastaccess.update_info self
42
+ Fastaccess.redis.set(redis_id, (response.is_a? String) ? response : response.to_json)
43
+ return response
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ ActiveRecord::Base.send :include, Fastaccess::ActsWithFastaccessOn
@@ -0,0 +1,204 @@
1
+ require 'set'
2
+
3
+ module Fastaccess
4
+ # This class contains the most
5
+ # relevant helpers methods, which
6
+ # doesn't need to be located elsewhere.
7
+ # (e.g. like mixins)
8
+ class Fastaccess
9
+
10
+ # the default options for the
11
+ # acts_with_fastaccess_on method.
12
+ ACTS_OPTIONS_DEFAULTS = {
13
+ :auto_update => true
14
+ }
15
+
16
+ @@fastaccess_on = Hash.new Set.new
17
+ @@last_updated = Hash.new
18
+ @@registered_options = Hash.new ACTS_OPTIONS_DEFAULTS
19
+
20
+ # accessor for the actual registration hash.
21
+ cattr_accessor :fastaccess_on
22
+
23
+ # hash for monitoring updates on registered objects.
24
+ cattr_accessor :last_updated
25
+
26
+ # hash for options of registered methods
27
+ cattr_accessor :registered_options
28
+
29
+ # registers a method, defined on a certain class,
30
+ # as being handled by fastaccess.
31
+ # @param [Class] class_name is the actual Class.
32
+ # @param [Symbol] method_name is the symbol
33
+ # denoting the actual method
34
+ def self.register_on(class_name, method_name, options={})
35
+ self.fastaccess_on[class_name] << method_name
36
+ id = options_id_for(class_name, method_name)
37
+ self.registered_options[id] = self.registered_options[id].merge(options)
38
+ end
39
+
40
+ # inquires if a certain method, which is
41
+ # defined on a given class, is registered
42
+ # with fastaccess.
43
+ # @param [Class] class_name is the actual Class.
44
+ # @param [Symbol] method_name is the symbol
45
+ # denoting the actual method
46
+ def self.registered?(class_name, method_name)
47
+ self.fastaccess_on[class_name].include? method_name
48
+ end
49
+
50
+ # gets the options for a class_name, method_name
51
+ # pair
52
+ # @param [Class] class_name is the actual Class.
53
+ # @param [Symbol] method_name is the symbol
54
+ # denoting the actual method
55
+ # @return [Hash] the options for the pair
56
+ def self.options_for(class_name, method_name)
57
+ id = options_id_for(class_name, method_name)
58
+ self.registered_options[id]
59
+ end
60
+
61
+ # checks if a class_instance seems to be
62
+ # up to date according to updated_at timestamp.
63
+ # This only works if the registered method
64
+ # is actually dependent (and only dependent)
65
+ # on model attributes.
66
+ # @param [Object] class_instance any Object,
67
+ # preferably a decendent of
68
+ # an actual Rails Model.
69
+ # @return [Boolean] is true if everything is up to date.
70
+ def self.update_check(class_instance)
71
+ id = self.id_for(class_instance)
72
+ return true if self.last_updated[id] == false
73
+ class_instance.updated_at == self.last_updated[id]
74
+ end
75
+
76
+ # updates the timestamp in the redis
77
+ # database with the one from class_instance.
78
+ # usually called after there was new content
79
+ # pushed into redis (or content was updated)
80
+ # @param [Object] class_instance any Object,
81
+ # preferably a decendent of
82
+ # an actual Rails Model.
83
+ def self.update_info(class_instance)
84
+ id = self.id_for(class_instance)
85
+ unless self.last_updated[id] == false
86
+ self.last_updated[id] = class_instance.updated_at
87
+ end
88
+ end
89
+
90
+ # creates a fastaccess id for a class_instance
91
+ # @param [Object] class_instance any Object,
92
+ # preferably a decendent of
93
+ # an actual Rails Model.
94
+ # @return [String] the identifier
95
+ def self.id_for(class_instance)
96
+ "#{class_instance.class}-#{class_instance.id}"
97
+ end
98
+
99
+ # creates the id for the registered_options hash
100
+ # @param [Class] class_name a class singleton object
101
+ # @param [Symbol] method_name is the identifying symbol of a method
102
+ # @return [String] the identifier
103
+ def self.options_id_for(class_name, method_name)
104
+ "#{class_name}-#{method_name}"
105
+ end
106
+
107
+ # returns the aliased name for
108
+ # any given method.
109
+ # @param [Symbol] method a symbol denoting the method.
110
+ # @return [Symbol] aliased method symbol.
111
+ def self.alias_for(method)
112
+ :"aliased_#{method}"
113
+ end
114
+
115
+ # setting content in redis
116
+ # @param [String] redis_id the id
117
+ # @param [Object] content should be basic content
118
+ # e.g. String, Hash, Array or Numbers.
119
+ def self.set(redis_id, content)
120
+ redis.set(redis_id, (content.is_a? String) ? content : content.to_json)
121
+ end
122
+
123
+ # getting redis content
124
+ # @param [String] redis_id the id
125
+ # @return [Object] stored content
126
+ def self.get(redis_id)
127
+ response = redis.get(redis_id)
128
+ begin
129
+ return JSON.parse response
130
+ rescue JSON::ParserError
131
+ return response
132
+ end
133
+ end
134
+
135
+ # manually update content in redis
136
+ # for a given object.
137
+ # @param [Object] obj any Object,
138
+ # preferably a decendent of
139
+ # an actual Rails Model.
140
+ # @param [Hash] options a simple hash
141
+ # @option options [Symbol] :on a certain registered
142
+ # method.
143
+ # @option options [Array] :arguments an array of
144
+ # arguments passed to
145
+ # the :on method or, if
146
+ # :on is not present, every
147
+ # method registered with fastaccess
148
+ # on the pertaining class.
149
+ def self.update_content(obj, options={})
150
+ class_name = obj.is_a?(Class) ? obj : obj.class
151
+ methods = if method = options[:on]
152
+ if registered? class_name, method
153
+ [method]
154
+ else
155
+ []
156
+ end
157
+ else
158
+ fastaccess_on[class_name]
159
+ end
160
+ methods.each do |method|
161
+ callable = obj.method( alias_for(method) )
162
+ content = if options[:arguments]
163
+ callable.call(*options[:arguments])
164
+ else
165
+ callable.call
166
+ end
167
+ self.set("#{method}_#{id_for(obj)}", content)
168
+ end
169
+ end
170
+
171
+ # setting up the environment for fastaccess
172
+ def self.setup(&block)
173
+ instance_eval &block if block_given?
174
+ end
175
+
176
+ # setting the global redis instance for fastaccess.
177
+ # @param [Redis] redis_instance The Connection to a redis server
178
+ def self.set_redis(redis_instance)
179
+ if redis_instance
180
+ @@redis = redis_instance
181
+ else
182
+ @@redis = $redis if $redis
183
+ end
184
+ end
185
+
186
+ # getting the redis instance
187
+ # @return [Redis] the instance
188
+ def self.redis
189
+ @@redis
190
+ end
191
+
192
+ # merges the, hopefully, reasonable
193
+ # defaults with the supplied options
194
+ # hash
195
+ # @param [Hash] options the pertaining options
196
+ # supplied by the user
197
+ # @return [Hash] the merged options hash
198
+ def self.merge_defaults(options)
199
+ ACTS_OPTIONS_DEFAULTS.merge(options)
200
+ end
201
+
202
+ end
203
+
204
+ end