authorize 0.0.1
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/.gitignore +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +20 -0
- data/README +155 -0
- data/Rakefile +25 -0
- data/TODO.txt +9 -0
- data/authorize.gemspec +25 -0
- data/generators/authorize/USAGE +8 -0
- data/generators/authorize/authorize_generator.rb +7 -0
- data/generators/authorize/templates/migrate/create_authorizations.rb +26 -0
- data/install.rb +1 -0
- data/lib/authorize.rb +2 -0
- data/lib/authorize/action_controller.rb +59 -0
- data/lib/authorize/action_view.rb +4 -0
- data/lib/authorize/active_record.rb +37 -0
- data/lib/authorize/bitmask.rb +84 -0
- data/lib/authorize/exceptions.rb +30 -0
- data/lib/authorize/graph.rb +4 -0
- data/lib/authorize/graph/directed_acyclic_graph.rb +10 -0
- data/lib/authorize/graph/directed_acyclic_graph_reverse_traverser.rb +27 -0
- data/lib/authorize/graph/directed_acyclic_graph_traverser.rb +30 -0
- data/lib/authorize/graph/directed_graph.rb +27 -0
- data/lib/authorize/graph/edge.rb +58 -0
- data/lib/authorize/graph/factory.rb +39 -0
- data/lib/authorize/graph/fixtures.rb +33 -0
- data/lib/authorize/graph/graph.rb +55 -0
- data/lib/authorize/graph/traverser.rb +89 -0
- data/lib/authorize/graph/undirected_graph.rb +14 -0
- data/lib/authorize/graph/vertex.rb +53 -0
- data/lib/authorize/permission.rb +97 -0
- data/lib/authorize/redis.rb +2 -0
- data/lib/authorize/redis/array.rb +36 -0
- data/lib/authorize/redis/base.rb +165 -0
- data/lib/authorize/redis/connection_manager.rb +88 -0
- data/lib/authorize/redis/connection_specification.rb +16 -0
- data/lib/authorize/redis/factory.rb +64 -0
- data/lib/authorize/redis/fixtures.rb +22 -0
- data/lib/authorize/redis/hash.rb +34 -0
- data/lib/authorize/redis/model_reference.rb +21 -0
- data/lib/authorize/redis/model_set.rb +19 -0
- data/lib/authorize/redis/set.rb +42 -0
- data/lib/authorize/redis/string.rb +17 -0
- data/lib/authorize/resource.rb +4 -0
- data/lib/authorize/resource_pool.rb +87 -0
- data/lib/authorize/role.rb +115 -0
- data/lib/authorize/test_helper.rb +42 -0
- data/lib/authorize/trustee.rb +4 -0
- data/lib/authorize/version.rb +3 -0
- data/rails/init.rb +5 -0
- data/tasks/authorize_tasks.rake +4 -0
- data/test/Rakefile +7 -0
- data/test/app/controllers/application_controller.rb +5 -0
- data/test/app/controllers/thingy_controller.rb +11 -0
- data/test/app/controllers/widgets_controller.rb +2 -0
- data/test/app/models/public.rb +14 -0
- data/test/app/models/user.rb +8 -0
- data/test/app/models/widget.rb +7 -0
- data/test/config/boot.rb +109 -0
- data/test/config/database.yml +25 -0
- data/test/config/environment.rb +28 -0
- data/test/config/environments/development.rb +4 -0
- data/test/config/environments/test.rb +0 -0
- data/test/config/initializers/mask.rb +1 -0
- data/test/config/initializers/redis.rb +8 -0
- data/test/config/routes.rb +5 -0
- data/test/db/.gitignore +1 -0
- data/test/db/schema.rb +26 -0
- data/test/log/.gitignore +2 -0
- data/test/public/javascripts/application.js +2 -0
- data/test/public/javascripts/controls.js +963 -0
- data/test/public/javascripts/dragdrop.js +972 -0
- data/test/public/javascripts/effects.js +1120 -0
- data/test/public/javascripts/prototype.js +4225 -0
- data/test/script/about +3 -0
- data/test/script/console +3 -0
- data/test/script/dbconsole +3 -0
- data/test/script/destroy +3 -0
- data/test/script/generate +3 -0
- data/test/script/performance/benchmarker +3 -0
- data/test/script/performance/profiler +3 -0
- data/test/script/performance/request +3 -0
- data/test/script/plugin +3 -0
- data/test/script/process/inspector +3 -0
- data/test/script/process/reaper +3 -0
- data/test/script/process/spawner +3 -0
- data/test/script/runner +3 -0
- data/test/script/server +3 -0
- data/test/test/fixtures/authorize/role_graph.yml +11 -0
- data/test/test/fixtures/permissions.yml +27 -0
- data/test/test/fixtures/redis/redis.yml +8 -0
- data/test/test/fixtures/redis/role_graph.yml +29 -0
- data/test/test/fixtures/roles.yml +28 -0
- data/test/test/fixtures/users.yml +12 -0
- data/test/test/fixtures/widgets.yml +12 -0
- data/test/test/functional/controller_class_test.rb +36 -0
- data/test/test/functional/controller_test.rb +46 -0
- data/test/test/test_helper.rb +35 -0
- data/test/test/unit/bitmask_test.rb +112 -0
- data/test/test/unit/fixture_test.rb +59 -0
- data/test/test/unit/graph_directed_acyclic_graph_reverse_traverser_test.rb +43 -0
- data/test/test/unit/graph_directed_acyclic_graph_traverser_test.rb +57 -0
- data/test/test/unit/graph_directed_graph_test.rb +66 -0
- data/test/test/unit/graph_edge_test.rb +53 -0
- data/test/test/unit/graph_graph_test.rb +50 -0
- data/test/test/unit/graph_traverser_test.rb +43 -0
- data/test/test/unit/graph_vertex_test.rb +57 -0
- data/test/test/unit/permission_test.rb +123 -0
- data/test/test/unit/redis_array_test.rb +60 -0
- data/test/test/unit/redis_connection_manager_test.rb +54 -0
- data/test/test/unit/redis_factory_test.rb +85 -0
- data/test/test/unit/redis_fixture_test.rb +18 -0
- data/test/test/unit/redis_hash_test.rb +43 -0
- data/test/test/unit/redis_model_reference_test.rb +39 -0
- data/test/test/unit/redis_set_test.rb +68 -0
- data/test/test/unit/redis_string_test.rb +25 -0
- data/test/test/unit/redis_test.rb +121 -0
- data/test/test/unit/resource_pool_test.rb +93 -0
- data/test/test/unit/resource_test.rb +33 -0
- data/test/test/unit/role_test.rb +143 -0
- data/test/test/unit/trustee_test.rb +35 -0
- data/test/tmp/.gitignore +2 -0
- data/uninstall.rb +1 -0
- metadata +319 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'authorize/redis'
|
2
|
+
|
3
|
+
module Authorize
|
4
|
+
module Graph
|
5
|
+
class UndirectedGraph < Graph
|
6
|
+
# Join two vertices symmetrically so that they become adjacent. Graphs built uniquely with
|
7
|
+
# this method will be undirected.
|
8
|
+
def join(id, v0, v1, *args)
|
9
|
+
edge_id = id || subordinate_key("_edges", true)
|
10
|
+
!!(edge(edge_id + "-01", v0, v1, *args) && edge(edge_id + "-10", v1, v0, *args))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Authorize
|
2
|
+
module Graph
|
3
|
+
class Vertex < Redis::Hash
|
4
|
+
def self.exists?(id)
|
5
|
+
super(subordinate_key(id, '_'))
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.load_all(namespace = name)
|
9
|
+
redis_glob = subordinate_key(namespace, '*', '_')
|
10
|
+
re = Regexp.new(subordinate_key(namespace, ".+(?=#{NAMESPACE_SEPARATOR})"))
|
11
|
+
keys = db.keys(redis_glob)
|
12
|
+
keys = keys.map{|m| m.slice(re)}
|
13
|
+
keys.map{|id| load(id)}
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(properties = {})
|
17
|
+
super()
|
18
|
+
# Because a degenerate vertex can have neither properties nor edges, we must store a marker to indicate existence
|
19
|
+
self.class.db.set(subordinate_key('_'), nil)
|
20
|
+
merge(properties) if properties.any?
|
21
|
+
end
|
22
|
+
|
23
|
+
def destroy
|
24
|
+
outbound_edges.each{|e| e.destroy}
|
25
|
+
outbound_edges.destroy
|
26
|
+
inbound_edges.each{|e| e.destroy}
|
27
|
+
inbound_edges.destroy
|
28
|
+
self.class.db.del(subordinate_key('_'))
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def adjacencies
|
33
|
+
outbound_edges.map(&:to)
|
34
|
+
end
|
35
|
+
alias neighbors adjacencies
|
36
|
+
|
37
|
+
def outbound_edges
|
38
|
+
@edges || Redis::ModelSet.new(subordinate_key('edge_ids'), Edge)
|
39
|
+
end
|
40
|
+
alias edges outbound_edges
|
41
|
+
|
42
|
+
# This index is required for efficient backlinking, such as when deleting a vertex.
|
43
|
+
def inbound_edges
|
44
|
+
@inbound_edges || Redis::ModelSet.new(subordinate_key('inbound_edge_ids'), Edge)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Visit this vertex via the given edge
|
48
|
+
def visit(edge, &block)
|
49
|
+
yield self
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'authorize/bitmask'
|
2
|
+
|
3
|
+
class Authorize::Permission < ActiveRecord::Base
|
4
|
+
class Mask < Authorize::Bitmask;end
|
5
|
+
|
6
|
+
set_table_name 'authorize_permissions'
|
7
|
+
# This is of questionable value given the specific implementation of mask attribute methods. It also requires
|
8
|
+
# table inspection, so we skip it if the table does not yet exist.
|
9
|
+
cache_attributes('mask') if table_exists?
|
10
|
+
|
11
|
+
belongs_to :_resource, :polymorphic => true, :foreign_type => 'resource_type', :foreign_key => 'resource_id'
|
12
|
+
belongs_to :role, :class_name => "Authorize::Role"
|
13
|
+
validates_presence_of :role
|
14
|
+
validates_presence_of :resource
|
15
|
+
|
16
|
+
before_save :set_mandatory_list_mode
|
17
|
+
|
18
|
+
# Returns the explicit authorizations over a subject. The resource can be any one of the following
|
19
|
+
# Object global permissions are returned
|
20
|
+
# <Resource Class> global and class permissions are returned
|
21
|
+
# <Resource Instance> global, class and instance permissions are returned
|
22
|
+
# This exists to simplify finding and creating global and class permissions. For resource instance
|
23
|
+
# permissions, use the standard Rails association (#permissions) created for authorizable resources.
|
24
|
+
named_scope :for, lambda {|resource|
|
25
|
+
resource_conditions = if (resource == Object) then
|
26
|
+
{:resource_id => nil, :resource_type => nil}
|
27
|
+
elsif resource.is_a?(Class) then
|
28
|
+
{:resource_id => nil, :resource_type => resource.to_s}
|
29
|
+
else
|
30
|
+
{:resource_id => resource.id, :resource_type => resource.class.to_s}
|
31
|
+
end
|
32
|
+
{:conditions => resource_conditions}
|
33
|
+
}
|
34
|
+
# Returns the effective permissions over a resource.
|
35
|
+
named_scope :over, lambda {|resource|
|
36
|
+
resource_conditions = if (resource == Object) then
|
37
|
+
{:resource_id => nil, :resource_type => nil}
|
38
|
+
elsif resource.is_a?(Class) then
|
39
|
+
c1 = sanitize_sql_hash_for_conditions(:resource_type => nil)
|
40
|
+
c2 = sanitize_sql_hash_for_conditions(:resource_type => resource.base_class.name, :resource_id => nil)
|
41
|
+
"#{c1} OR (#{c2})"
|
42
|
+
else
|
43
|
+
c1 = sanitize_sql_hash_for_conditions(:resource_type => nil)
|
44
|
+
c2 = sanitize_sql_hash_for_conditions(:resource_type => resource.class.base_class.name)
|
45
|
+
c3 = sanitize_sql_hash_for_conditions(:resource_id => resource.quoted_id)
|
46
|
+
c4 = sanitize_sql_hash_for_conditions(:resource_id => nil)
|
47
|
+
"#{c1} OR (#{c2} AND (#{c3} OR #{c4}))"
|
48
|
+
end
|
49
|
+
{:conditions => resource_conditions}
|
50
|
+
}
|
51
|
+
named_scope :to_do, lambda {|mask| {:conditions =>"mask & #{mask.to_i} = #{mask.to_i}"}}
|
52
|
+
named_scope :as, lambda {|roles| {:conditions => {:role_id => roles.map(&:id)}}}
|
53
|
+
named_scope :global, :conditions => {:resource_type => nil, :resource_id => nil}
|
54
|
+
|
55
|
+
# Determine if the aggregate mask includes the requested modes.
|
56
|
+
def self.permit?(requested_modes)
|
57
|
+
requested_modes.subset?(aggregate_mask)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Find the aggregate permission mask for the current scope
|
61
|
+
# This calculation could be more effectively performed at the database using an aggregate function. For
|
62
|
+
# MySQL, a bit_or function exists. For SQLite3, it is necessary to code an extension. For an example,
|
63
|
+
# see: http://snippets.dzone.com/posts/show/3717
|
64
|
+
def self.aggregate_mask
|
65
|
+
Mask.new(all.inject(Set.new){|memo, p| memo | p.mask})
|
66
|
+
end
|
67
|
+
|
68
|
+
# Because the list mode is always assumed to be set for performance, we expose that assumption explicitly.
|
69
|
+
def set_mandatory_list_mode
|
70
|
+
self['mask'] |= Mask.name_values[:list]
|
71
|
+
@attributes_cache.delete('mask')
|
72
|
+
end
|
73
|
+
|
74
|
+
# Virtual attribute that expands the common belongs_to association with a three-level hierarchy
|
75
|
+
def resource
|
76
|
+
return Object unless resource_type
|
77
|
+
return resource_type.constantize unless resource_id
|
78
|
+
return _resource
|
79
|
+
end
|
80
|
+
|
81
|
+
def resource=(res)
|
82
|
+
return self._resource = res unless res.kind_of?(Class)
|
83
|
+
self.resource_id = nil
|
84
|
+
return self[:resource_type] = nil if res == Object
|
85
|
+
return self[:resource_type] = res.to_s
|
86
|
+
end
|
87
|
+
|
88
|
+
def mask(reload = false)
|
89
|
+
cached = @attributes_cache['mask'] # undocumented hash of cache nicely invalidated by write_attribute
|
90
|
+
return cached if cached && !reload
|
91
|
+
@attributes_cache['mask'] = Mask.new(read_attribute('mask')) # Ensure we always return a Mask instance
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_s
|
95
|
+
"#{role} over #{resource} (#{mask})"
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Authorize
|
2
|
+
module Redis
|
3
|
+
class Array < Base
|
4
|
+
undef to_a # In older versions of Ruby, Object#to_a is invoked and #method_missing is never called.
|
5
|
+
|
6
|
+
def valid?
|
7
|
+
%w(none list).include?(db.type(id))
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](index)
|
11
|
+
if index.respond_to?(:first)
|
12
|
+
db.lrange(id, index.first, index.last)
|
13
|
+
else
|
14
|
+
db.lindex(id, index)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def []=(index, v)
|
19
|
+
db.lset(id, index, v)
|
20
|
+
end
|
21
|
+
|
22
|
+
def push(v)
|
23
|
+
db.rpush(id, v)
|
24
|
+
end
|
25
|
+
alias << push
|
26
|
+
|
27
|
+
def pop
|
28
|
+
db.rpop(id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def __getobj__
|
32
|
+
db.lrange(id, 0, -1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module Authorize
|
2
|
+
module Redis
|
3
|
+
# The key feature of this module is that it presents a coherent view of the database in memory. For
|
4
|
+
# each database entry, at most one in-memory Ruby object will exist, and all state for the object will
|
5
|
+
# be atomically persisted to the database. This behavior introduces the following constraints:
|
6
|
+
# 1. The database is viewed through an identity map (http://en.wikipedia.org/wiki/Identity_map) to
|
7
|
+
# ensure in-thread coherency. Consequently, the record's key must be known prior to initialization,
|
8
|
+
# allowing new objects to be instantiated only if no previously instantiated object with that key is
|
9
|
+
# already in memory.
|
10
|
+
# 2. In order to allow Redis::Base#initialize to set values (which are atomically persisted), the id must
|
11
|
+
# be available at the _start_ of initialization. This is accomplished by overriding Redis.new and
|
12
|
+
# assigning the id immediately after allocation.
|
13
|
+
# TODO: YAML serialization (http://groups.google.com/group/comp.lang.ruby/browse_thread/thread/c855253c9d8f482e)
|
14
|
+
class Base
|
15
|
+
NAMESPACE_SEPARATOR = '::'
|
16
|
+
@base = true
|
17
|
+
class << self
|
18
|
+
attr_writer :logger
|
19
|
+
attr_writer :connection_specification
|
20
|
+
|
21
|
+
# Should this class establish a connection instead of relying on a superclass' connection?
|
22
|
+
def connection_base?
|
23
|
+
@base || @connection_specification
|
24
|
+
end
|
25
|
+
|
26
|
+
# Search up the inheritance chain for a manager unless a connection is specified here.
|
27
|
+
def connection_manager
|
28
|
+
@manager ||= (connection_base? ? Redis::ConnectionManager.new(@connection_specification) : superclass.connection_manager)
|
29
|
+
end
|
30
|
+
|
31
|
+
def connection
|
32
|
+
connection_manager.connection
|
33
|
+
end
|
34
|
+
alias db connection
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.logger
|
38
|
+
@logger ||= (@base ? nil : superclass.logger)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.subordinate_key(*keys)
|
42
|
+
keys.compact.join(NAMESPACE_SEPARATOR)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.next_counter(key)
|
46
|
+
db.incr(key)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.generate_key
|
50
|
+
subordinate_key(name, next_counter(name))
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.index
|
54
|
+
@index ||= ::Hash.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.exists?(id)
|
58
|
+
db.exists(id)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Load all model objects in the given namespace
|
62
|
+
def self.load_all(namespace = name)
|
63
|
+
redis_glob = subordinate_key(namespace, '*')
|
64
|
+
re = Regexp.new(subordinate_key(namespace, ".+(?=#{NAMESPACE_SEPARATOR})"))
|
65
|
+
db.keys(redis_glob).map{|m| m.slice(re)}.map{|id| load(id)}
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.new(id = nil, *args, &block)
|
69
|
+
id ||= generate_key
|
70
|
+
index[id] = allocate.tap do |o|
|
71
|
+
o.instance_variable_set(:@id, id)
|
72
|
+
o.send(:initialize, *args, &block)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.load(id)
|
77
|
+
index[id] ||= allocate.tap do |o|
|
78
|
+
o.instance_variable_set(:@id, id)
|
79
|
+
o.send(:reload)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
def self._load(id);load(id);end
|
83
|
+
|
84
|
+
attr_reader :id
|
85
|
+
alias to_s id
|
86
|
+
|
87
|
+
def logger
|
88
|
+
self.class.logger
|
89
|
+
end
|
90
|
+
|
91
|
+
def db
|
92
|
+
self.class.db
|
93
|
+
end
|
94
|
+
|
95
|
+
def eql?(other)
|
96
|
+
id.eql?(other.id) && other.is_a?(self.class)
|
97
|
+
end
|
98
|
+
|
99
|
+
def hash
|
100
|
+
id.hash
|
101
|
+
end
|
102
|
+
|
103
|
+
def ==(other)
|
104
|
+
id.eql?(other.id) && other.is_a?(self.class)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Note that requesting a counter value "steals" from the class counter.
|
108
|
+
def subordinate_key(name, counter = false)
|
109
|
+
k = self.class.subordinate_key(id, name)
|
110
|
+
counter ? self.class.subordinate_key(k, self.class.next_counter(k)) : k
|
111
|
+
end
|
112
|
+
|
113
|
+
# This hook restores a re-instantiated object that has previously been initialized and then persisted.
|
114
|
+
# Non-idempotent operations should be used with great care.
|
115
|
+
def reload;end
|
116
|
+
|
117
|
+
def _dump(depth = nil)
|
118
|
+
id
|
119
|
+
end
|
120
|
+
|
121
|
+
# Emit this Redis object with a a magic type and simple scalar identifier. The (poorly documented) "type id" format
|
122
|
+
# allows for a succinct one-line YAML expression for a Redis instance (no indented attributes hash required) which in
|
123
|
+
# turn simplifies automatic YAMLification of collections of Redis objects. Arguably, it's more readable as well.
|
124
|
+
def to_yaml(opts = {})
|
125
|
+
YAML.quick_emit(self.id, opts) {|out| out.scalar("tag:hapgoods.com,2010-08-11:#{self.class.name}", id)}
|
126
|
+
end
|
127
|
+
|
128
|
+
def destroy
|
129
|
+
db.del(id) # This operation will remove all native Redis types (String, Hash, List, Set, etc.) in one shot.
|
130
|
+
self.class.index.delete(id)
|
131
|
+
freeze
|
132
|
+
end
|
133
|
+
|
134
|
+
def exists?
|
135
|
+
self.class.exists?(id)
|
136
|
+
end
|
137
|
+
|
138
|
+
def valid?
|
139
|
+
raise "Abstract class requires implementation"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Methods that don't change the state of the object can safely delegate to a Ruby proxy object
|
143
|
+
def __getobj__
|
144
|
+
raise "Abstract class requires implementation"
|
145
|
+
end
|
146
|
+
|
147
|
+
def method_missing(m, *args, &block)
|
148
|
+
proxy = __getobj__ # Performance tweak
|
149
|
+
return super unless proxy.respond_to?(m) # If there is going to be an explosion, let superclass handle it.
|
150
|
+
proxy.freeze.__send__(m, *args, &block) # Ensure no state can be changed and send the method on its way.
|
151
|
+
end
|
152
|
+
|
153
|
+
def respond_to?(m, include_private = false)
|
154
|
+
return true if super
|
155
|
+
__getobj__.respond_to?(m, include_private)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
YAML.add_domain_type("hapgoods.com,2010-08-11", "") do |type, val|
|
162
|
+
md = /tag:(.*),([^:]*):((?:\w+)(?:::\w+)*)/.match(type)
|
163
|
+
domain, version, klass = *md[1..3]
|
164
|
+
klass.constantize.load(val)
|
165
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Authorize
|
5
|
+
module Redis
|
6
|
+
# This class arbitrates access to a Redis server ensuring that threads don't concurrently access the same connection
|
7
|
+
# http://yehudakatz.com/2010/08/14/threads-in-ruby-enough-already/
|
8
|
+
# http://blog.headius.com/2008/08/qa-what-thread-safe-rails-means.html
|
9
|
+
# Inspired by the ConnectionPool class in Rails 2.3
|
10
|
+
# Notes on thread-safety: Because instances of this class are expected to be shared
|
11
|
+
# across threads, access to all non-local variables and "constants" need to be synchronized.
|
12
|
+
# We assume that Hash read/assign operations are thread-safe. We also require that the
|
13
|
+
# value returned by #current_connection_id not be shared across threads.
|
14
|
+
class ConnectionManager
|
15
|
+
class ConnectionError < RuntimeError; end
|
16
|
+
|
17
|
+
attr_reader :pool
|
18
|
+
|
19
|
+
# Creates a new ConnectionPool object. +specification+ is a ConnectionSpecification
|
20
|
+
# object which describes database connection parameters
|
21
|
+
def initialize(specification, options = {})
|
22
|
+
@options = {:size => 5}.merge(options)
|
23
|
+
@pool = ResourcePool.new(@options[:size], lambda {specification.connect!})
|
24
|
+
@connection_map = {} # Connections mapped to threads
|
25
|
+
@mutex = Monitor.new
|
26
|
+
end
|
27
|
+
|
28
|
+
# Retrieve the connection associated with the current thread, or checkout one from the pool as required.
|
29
|
+
# #connection can be called any number of times; the connection is held in a hash with a thread-specific key.
|
30
|
+
def acquire_connection
|
31
|
+
@connection_map[current_connection_id] ||= @pool.checkout(10)
|
32
|
+
end
|
33
|
+
alias connection acquire_connection
|
34
|
+
|
35
|
+
# Signal that the thread is finished with the current connection.
|
36
|
+
# #release_connection releases the connection-thread association
|
37
|
+
# and returns the connection to the pool.
|
38
|
+
def release_connection
|
39
|
+
c = @connection_map.delete(current_connection_id)
|
40
|
+
@pool.checkin(c) if c
|
41
|
+
end
|
42
|
+
|
43
|
+
# Checks out a connection from the pool, yields it to a block and checks it back into the pool when the block finishes.
|
44
|
+
def with_connection
|
45
|
+
c = @pool.checkout
|
46
|
+
yield c
|
47
|
+
ensure
|
48
|
+
@pool.checkin(c)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Identifies connections in the death grip of defunct threads, removes them from the map and checks them back into the pool
|
52
|
+
# Because this method operates across connections for multiple threads (not just the current thread), concurrent execution
|
53
|
+
# needs to be synchronized to be thread-safe.
|
54
|
+
def recover_unused_connections
|
55
|
+
tids = Thread.list.select{|t| t.alive?}.map(&:object_id)
|
56
|
+
@mutex.synchronize do
|
57
|
+
cids = @connection_map.keys
|
58
|
+
(cids - tids).each do |sid|
|
59
|
+
c = @connection_map.delete(sid)
|
60
|
+
@pool.checkin(c)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Expire stale connections
|
66
|
+
def expire_stale_connections!
|
67
|
+
@pool.expire do |connection, reserved_flag|
|
68
|
+
!connection.client.connected?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Revert to a freshly initialized state
|
73
|
+
def reset!
|
74
|
+
@mutex.synchronize do
|
75
|
+
@pool.clear!
|
76
|
+
@connection_map.clear
|
77
|
+
end
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
# In order to guarantee thread-safety, this value must never be shared across threads.
|
83
|
+
def current_connection_id #:nodoc:
|
84
|
+
Thread.current.object_id
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|