flockdb 0.3.0

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.
@@ -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