flockdb 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ pkg/*
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ FlockDB Client is Copyright (C) 2010 Twitter, Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not
4
+ use this work except in compliance with the License. You may obtain a copy of
5
+ the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+ License for the specific language governing permissions and limitations under
13
+ the License.
@@ -0,0 +1,60 @@
1
+ FlockDB ruby client
2
+
3
+ FlockDB is a distributed graph database capable of storing millions of
4
+ nodes, billions of edges, and handling tens of thousands of operations
5
+ per second. Interact with it in ruby with this client.
6
+
7
+
8
+ INSTALLATION
9
+ ------------
10
+
11
+ gem install flockdb
12
+
13
+ FlockDB depends on thrift and thrift_client, which will install as
14
+ part of the gem's dependencies. Note that thrift has a native C
15
+ extension which it will need to compile.
16
+
17
+
18
+ USAGE
19
+ -----
20
+
21
+ Instantiate a client:
22
+
23
+ flockdb = Flock.new 'localhost:7915, :graphs => { :follows => 1, :blocks => 2 }
24
+
25
+ Flock.new is a convenience shortcut for Flock::Client.new. Flock.new
26
+ expects a list (or a single) servers as the first argument, and an
27
+ options hash. Flock::Client.new takes the same options as ThriftClient
28
+ for configuring the raw thrift client, as well as :graphs for mapping
29
+ ruby symbols to the corresponding graph ids. See [thrift_client's
30
+ documentation](http://github.com/fauna/thrift_client) for information
31
+ on the other options it takes.
32
+
33
+ Edge manipulation:
34
+
35
+ flockdb.add(1, :follows, 2)
36
+ flockdb.remove(1, :blocks, 2)
37
+
38
+ Perform a query:
39
+
40
+ flockdb.select(1, :follows, nil).to_a
41
+
42
+ The client supports a rich query api to build up complex set operations:
43
+
44
+ flockdb.select(nil, :follows, 1).difference(flockdb.select(1, :follows, nil).intersect(2, :follows, nil)).to_a
45
+
46
+
47
+ CONTRIBUTORS
48
+ ------------
49
+
50
+ Nick Kallen
51
+ Matt Freels
52
+ Rael Dornfest
53
+
54
+
55
+ LICENSE
56
+ -------
57
+
58
+ Copyright (C) 2010 Twitter, Inc.
59
+
60
+ This work is licensed under the Apache License, Version 2.0. See LICENSE for details.
@@ -0,0 +1,34 @@
1
+ ROOT_DIR = File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'rubygems' rescue nil
4
+ require 'rake'
5
+ require 'spec/rake/spectask'
6
+
7
+ task :default => :spec
8
+
9
+ desc "Run all specs in spec directory."
10
+ Spec::Rake::SpecTask.new(:spec) do |t|
11
+ t.spec_opts = ['--options', "\"#{ROOT_DIR}/spec/spec.opts\""]
12
+ t.spec_files = FileList['spec/**/*_spec.rb']
13
+ end
14
+
15
+ # gemification with jeweler
16
+ begin
17
+ require 'jeweler'
18
+ Jeweler::Tasks.new do |gemspec|
19
+ gemspec.name = "flockdb"
20
+ gemspec.summary = "Ruby Flock client"
21
+ gemspec.description = "Get your flock on in Ruby."
22
+ gemspec.email = "freels@twitter.com"
23
+ gemspec.homepage = "http://twitter.com"
24
+ gemspec.authors = ["Matt Freels", "Rael Dornfest", "Nick Kallen"]
25
+ gemspec.add_dependency 'thrift', '>= 0.2.0'
26
+ gemspec.add_dependency 'thrift_client', '>= 0.4.1'
27
+
28
+ # development
29
+ gemspec.add_development_dependency 'rspec'
30
+ gemspec.add_development_dependency 'rr'
31
+ end
32
+ rescue LoadError
33
+ puts "Jeweler not available. Install it with: gem install jeweler"
34
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -0,0 +1,82 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{flockdb}
8
+ s.version = "0.3.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Matt Freels", "Rael Dornfest", "Nick Kallen"]
12
+ s.date = %q{2010-04-19}
13
+ s.description = %q{Get your flock on in Ruby.}
14
+ s.email = %q{freels@twitter.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".gitignore",
21
+ "LICENSE",
22
+ "README.md",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "flockdb.gemspec",
26
+ "lib/flock.rb",
27
+ "lib/flock/client.rb",
28
+ "lib/flock/mixins/sizeable.rb",
29
+ "lib/flock/mock_service.rb",
30
+ "lib/flock/operation.rb",
31
+ "lib/flock/operations/complex_operation.rb",
32
+ "lib/flock/operations/execute_operation.rb",
33
+ "lib/flock/operations/execute_operations.rb",
34
+ "lib/flock/operations/query_term.rb",
35
+ "lib/flock/operations/select_operation.rb",
36
+ "lib/flock/operations/simple_operation.rb",
37
+ "lib/flock/service.rb",
38
+ "lib/flock/thrift/edges.rb",
39
+ "lib/flock/thrift/edges_types.rb",
40
+ "lib/flock/thrift/flock_constants.rb",
41
+ "lib/flock/thrift/flock_types.rb",
42
+ "lib/flock/thrift/shards.rb",
43
+ "lib/flockdb.rb",
44
+ "spec/flock_spec.rb",
45
+ "spec/mock_service_spec.rb",
46
+ "spec/spec.opts",
47
+ "spec/spec_helper.rb"
48
+ ]
49
+ s.homepage = %q{http://twitter.com}
50
+ s.rdoc_options = ["--charset=UTF-8"]
51
+ s.require_paths = ["lib"]
52
+ s.rubygems_version = %q{1.3.6}
53
+ s.summary = %q{Ruby Flock client}
54
+ s.test_files = [
55
+ "spec/flock_spec.rb",
56
+ "spec/mock_service_spec.rb",
57
+ "spec/spec_helper.rb"
58
+ ]
59
+
60
+ if s.respond_to? :specification_version then
61
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
62
+ s.specification_version = 3
63
+
64
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
65
+ s.add_runtime_dependency(%q<thrift>, [">= 0.2.0"])
66
+ s.add_runtime_dependency(%q<thrift_client>, [">= 0.4.1"])
67
+ s.add_development_dependency(%q<rspec>, [">= 0"])
68
+ s.add_development_dependency(%q<rr>, [">= 0"])
69
+ else
70
+ s.add_dependency(%q<thrift>, [">= 0.2.0"])
71
+ s.add_dependency(%q<thrift_client>, [">= 0.4.1"])
72
+ s.add_dependency(%q<rspec>, [">= 0"])
73
+ s.add_dependency(%q<rr>, [">= 0"])
74
+ end
75
+ else
76
+ s.add_dependency(%q<thrift>, [">= 0.2.0"])
77
+ s.add_dependency(%q<thrift_client>, [">= 0.4.1"])
78
+ s.add_dependency(%q<rspec>, [">= 0"])
79
+ s.add_dependency(%q<rr>, [">= 0"])
80
+ end
81
+ end
82
+
@@ -0,0 +1,40 @@
1
+ # external dependencies
2
+ require 'thrift'
3
+ require 'thrift_client'
4
+
5
+ require 'flock/mixins/sizeable'
6
+
7
+ # thrift sources. load order is important.
8
+ require 'flock/thrift/flock_types'
9
+ require 'flock/thrift/flock_constants'
10
+ require 'flock/thrift/shards'
11
+ require 'flock/thrift/edges_types'
12
+ require 'flock/thrift/edges'
13
+
14
+ require 'flock/operation'
15
+ require 'flock/operations/query_term'
16
+ require 'flock/operations/select_operation'
17
+ require 'flock/operations/complex_operation'
18
+ require 'flock/operations/simple_operation'
19
+ require 'flock/operations/execute_operation'
20
+ require 'flock/operations/execute_operations'
21
+ require 'flock/service'
22
+ require 'flock/client'
23
+
24
+ module Flock
25
+ autoload :MockService, 'flock/mock_service'
26
+
27
+ class UnknownGraphError < FlockException
28
+ def initialize(graph)
29
+ super("Unable to look up id for graph #{graph.inspect}. Register graphs with Flock.graphs = <graph list>.")
30
+ end
31
+ end
32
+
33
+ class << self
34
+ attr_accessor :default_service_class, :graphs
35
+
36
+ def new(*args)
37
+ Flock::Client.new(*args)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,124 @@
1
+ class Flock::Client
2
+
3
+ attr_accessor :graphs
4
+ attr_reader :service
5
+
6
+ class << self
7
+ def service_class; Flock.default_service_class ||= Flock::Service end
8
+ end
9
+
10
+ # takes arguments a list of servers and an options hash to pass to the default service_class,
11
+ # or a service itself
12
+ def initialize(servers = nil, options = {})
13
+ if graphs = (options.delete(:graphs) || Flock.graphs)
14
+ @graphs = graphs
15
+ end
16
+
17
+ @service =
18
+ if servers.nil? or servers.is_a? Array or servers.is_a? String
19
+ self.class.service_class.new(servers, options)
20
+ else
21
+ servers
22
+ end
23
+ end
24
+
25
+
26
+ # local results cache
27
+
28
+ def cache_locally
29
+ @cache = {}
30
+ yield
31
+ ensure
32
+ @cache = nil
33
+ end
34
+
35
+ def _cache(op, query)
36
+ return yield unless @cache
37
+ @cache[[op, query]] ||= yield
38
+ end
39
+
40
+ def _cache_clear
41
+ @cache = {} if @cache
42
+ end
43
+
44
+
45
+ # queries
46
+
47
+ def select(*query)
48
+ Flock::SimpleOperation.new(self, _query_args(query))
49
+ end
50
+
51
+ def contains(*query)
52
+ _cache :contains, query do
53
+ select(*query).any?
54
+ end
55
+ end
56
+
57
+ def size(*query)
58
+ _cache :size, query do
59
+ select(*query).size
60
+ end
61
+ end
62
+ alias count size
63
+
64
+
65
+ # edge manipulation
66
+
67
+ def update(method, source_id, graph, destination_id, priority = Flock::Priority::High)
68
+ _cache_clear
69
+ ops = current_transaction || Flock::ExecuteOperations.new(@service, priority)
70
+ ops.send(method, *_query_args([source_id, graph, destination_id]))
71
+ ops.apply unless in_transaction?
72
+ end
73
+
74
+ [:add, :remove, :archive, :unarchive].each do |method|
75
+ class_eval "def #{method}(*args); update(#{method.inspect}, *args) end", __FILE__, __LINE__
76
+ end
77
+
78
+ def transaction(priority = Flock::Priority::High, &block)
79
+ new_transaction = !in_transaction?
80
+
81
+ ops =
82
+ if new_transaction
83
+ Thread.current[:edge_transaction] = Flock::ExecuteOperations.new(@service, priority)
84
+ else
85
+ current_transaction
86
+ end
87
+
88
+ result = yield self if block.arity == 1
89
+ ops.apply if new_transaction
90
+ result
91
+
92
+ ensure
93
+ Thread.current[:edge_transaction] = nil if new_transaction
94
+ end
95
+
96
+ def current_transaction
97
+ Thread.current[:edge_transaction]
98
+ end
99
+
100
+ def in_transaction?
101
+ !!Thread.current[:edge_transaction]
102
+ end
103
+
104
+
105
+ # graph name lookup utility methods
106
+
107
+ def _lookup_graph(key)
108
+ if @graphs.nil? or key.is_a? Integer
109
+ key
110
+ else
111
+ @graphs[key] or raise UnknownGraphError.new(key)
112
+ end
113
+ end
114
+
115
+ def _query_args(args)
116
+ source, graph, destination = *((args.length == 1) ? args.first : args)
117
+ [_node_arg(source), _lookup_graph(graph), _node_arg(destination)]
118
+ end
119
+
120
+ def _node_arg(node)
121
+ return node.map {|n| n.to_i if n } if node.respond_to? :map
122
+ node.to_i if node
123
+ end
124
+ end
@@ -0,0 +1,17 @@
1
+ module Flock
2
+ module Mixins
3
+ module Sizeable
4
+ def any?(&block)
5
+ if !block_given?
6
+ size > 0
7
+ else
8
+ map(&block).any?
9
+ end
10
+ end
11
+
12
+ def empty?
13
+ size == 0
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,178 @@
1
+ module Flock
2
+ module MockService
3
+ extend self
4
+
5
+ attr_accessor :timeout, :fixtures
6
+
7
+ def clear
8
+ @sources = @destinations = nil
9
+ end
10
+
11
+ def load(fixtures = nil)
12
+ fixtures ||= self.fixtures or raise "No flock fixtures specified. either pass fixtures to load, or set Flock::MockService.fixtures."
13
+
14
+ clear
15
+
16
+ fixtures.each do |fixture|
17
+ file, graph_id, source, destination = fixture.values_at(:file, :graph, :source, :destination)
18
+
19
+ fixtures_data = YAML::load(ERB.new(File.open(file, 'r').read).result(binding)).sort
20
+ fixtures_data.each do |key, value|
21
+ sources[[value[destination], graph_id]] << value[source]
22
+ destinations[[value[source], graph_id]] << value[destination]
23
+ end
24
+ end
25
+ end
26
+
27
+ def inspect
28
+ "Flock::MockService: " + @sources.inspect + " - " + @destinations.inspect
29
+ end
30
+
31
+ def execute(operations)
32
+ operations = operations.operations
33
+ operations.each do |operation|
34
+ term = operation.term
35
+ backward_data_source, forward_data_source = term.is_forward ? [sources, destinations] : [destinations, sources]
36
+ backward_archived_data_source, forward_archived_data_source = term.is_forward ? [archived_sources, archived_destinations] : [archived_destinations, archived_sources]
37
+ source_id, graph_id = term.source_id, term.graph_id
38
+ destination_ids = term.destination_ids && term.destination_ids.unpack("Q*")
39
+ case operation.operation_type
40
+ when Edges::ExecuteOperationType::Add
41
+ if destination_ids.nil?
42
+ backward_archived_data_source.delete([source_id, graph_id]).to_a.each do |n|
43
+ (forward_data_source[[n, graph_id]] << source_id).uniq!
44
+ (backward_data_source[[source_id, graph_id]] << n).uniq!
45
+ forward_archived_data_source[[n, graph_id]].delete(source_id)
46
+ end
47
+ else
48
+ destination_ids.each do |destination_id|
49
+ backward_data_source[[destination_id, graph_id]] << source_id
50
+ forward_data_source[[source_id, graph_id]] << destination_id
51
+ end
52
+ end
53
+ when Edges::ExecuteOperationType::Remove
54
+ destination_ids.each do |destination_id|
55
+ backward_data_source[[destination_id, graph_id]].delete(source_id)
56
+ forward_data_source[[source_id, graph_id]].delete(destination_id)
57
+ end
58
+ when Edges::ExecuteOperationType::Archive
59
+ if destination_ids.nil?
60
+ backward_data_source.delete([source_id, graph_id]).to_a.each do |n|
61
+ forward_archived_data_source[[n, graph_id]] << source_id
62
+ backward_archived_data_source[[source_id, graph_id]] << n
63
+ forward_data_source[[n, graph_id]].delete(source_id)
64
+ end
65
+ else
66
+ raise "not yet implemented"
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ def select(select_operations, page)
73
+ iterate(select_query(select_operations), page)
74
+ end
75
+
76
+ def count(select_operations)
77
+ select_query(select_operations).size
78
+ end
79
+
80
+ # FIXME
81
+ def counts_of_sources_for(destination_ids, graph_id)
82
+ destination_ids.unpack("Q*").map do |destination_id|
83
+ (sources[[destination_id, graph_id]] || []).size
84
+ end.pack("I*")
85
+ end
86
+
87
+ private
88
+ def select_query(select_operations)
89
+ stack = []
90
+ select_operations.each do |select_operation|
91
+ case select_operation.operation_type
92
+ when Edges::SelectOperationType::SimpleQuery
93
+ query_term = select_operation.term
94
+ data_source = query_term.is_forward ? destinations : sources
95
+ data = data_source[[query_term.source_id, query_term.graph_id]]
96
+ if query_term.destination_ids
97
+ data &= query_term.destination_ids.unpack("Q*")
98
+ end
99
+ stack.push(data)
100
+ when Edges::SelectOperationType::Intersection
101
+ stack.push(stack.pop & stack.pop)
102
+ when Edges::SelectOperationType::Union
103
+ stack.push(stack.pop | stack.pop)
104
+ when Edges::SelectOperationType::Difference
105
+ operand2 = stack.pop
106
+ operand1 = stack.pop
107
+ stack.push(operand1 - operand2)
108
+ end
109
+ end
110
+ stack.pop
111
+ end
112
+
113
+ private
114
+
115
+ def iterate(data, page)
116
+ return empty_result if page.cursor == Flock::CursorEnd
117
+
118
+ start = if page.cursor < Flock::CursorStart
119
+ [-page.cursor - page.count, 0].max
120
+ else
121
+ page.cursor == Flock::CursorStart ? 0 : page.cursor
122
+ end
123
+ rv = data.slice(start, page.count)
124
+ next_cursor = (start + page.count >= data.size) ? Flock::CursorEnd : start + page.count
125
+ prev_cursor = (start <= 0) ? Flock::CursorEnd : -start
126
+
127
+ result = Flock::Results.new
128
+ result.ids = Array(rv).pack("Q*")
129
+ result.next_cursor = next_cursor
130
+ result.prev_cursor = prev_cursor
131
+ result
132
+ end
133
+
134
+ def empty_result
135
+ @empty_result ||= begin
136
+ empty_result = Flock::Results.new
137
+ empty_result.ids = ""
138
+ empty_result.next_cursor = Flock::CursorEnd
139
+ empty_result.prev_cursor = Flock::CursorEnd
140
+ empty_result
141
+ end
142
+ end
143
+
144
+ def sources
145
+ @sources ||= Hash.new do |h, k|
146
+ h[k] = []
147
+ end
148
+ end
149
+
150
+ def destinations
151
+ @destinations ||= Hash.new do |h, k|
152
+ h[k] = []
153
+ end
154
+ end
155
+
156
+ def archived_sources
157
+ @archived_sources ||= Hash.new do |h, k|
158
+ h[k] = []
159
+ end
160
+ end
161
+
162
+ def archived_destinations
163
+ @archived_destinations ||= Hash.new do |h, k|
164
+ h[k] = []
165
+ end
166
+ end
167
+
168
+ # deprecated
169
+ public
170
+ def offset_sources_for(destination_id, graph_id, offset, count)
171
+ (sources[[destination_id, graph_id]].slice(offset, count) || []).pack("Q*")
172
+ end
173
+
174
+ def offset_destinations_for(source_id, graph_id, offset, count)
175
+ (destinations[[source_id, graph_id]].slice(offset, count) || []).pack("Q*")
176
+ end
177
+ end
178
+ end