tiny_ds 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009-2010 Takeru Sasaki, sasaki.takeru@gmail.com
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.
data/README.rdoc ADDED
@@ -0,0 +1,25 @@
1
+ * TinyDS -- tiny datastore library for GAE/JRuby
2
+ github: http://github.com/takeru/tiny_ds
3
+
4
+ - CRUD like a ActiveRecord or DataMapepr
5
+ - set parent, key, id, name, and find by these, YES entity-group-transaction!!
6
+ - BASE transaction : http://blog.notdot.net/2009/9/Distributed-Transactions-on-App-Engine
7
+ - query : very thin wrapper of low level API.
8
+ - Shout demo app. (sinatra)
9
+ - some specs..
10
+
11
+ ** run shout demo
12
+ % dev_appserver.rb .
13
+
14
+ ** how to run specs
15
+ install jruby 1.4.0
16
+ % source set_classpath_for_jruby.sh (set classpath, please read spec/spec_helper.rb)
17
+ % jruby -S spec -c -b spec/basic_spec.rb
18
+ or
19
+ % jruby -e "ENV['RSPEC']='true'; ENV['AUTOTEST']='true'; system('jruby -S autotest', *ARGV)"
20
+
21
+ ** demo
22
+ Rubyist Social Graph : http://rubyist-sg.appspot.com/ is running with GAE/JRuby+Rails2.3.5+TinyDS.
23
+
24
+ ** author
25
+ takeru : sasaki.takeru@gmail.com, twitter:@urekat (almost japanese!), blog:http://d.hatena.ne.jp/urekat
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+ require 'rubygems/specification'
5
+ require 'date'
6
+
7
+ require File.dirname(__FILE__) + '/lib/tiny_ds/version'
8
+
9
+ # set up pretty rdoc if possible
10
+ begin
11
+ gem 'rdoc'
12
+ require 'sdoc'
13
+ ENV['RDOCOPT'] = '-T lightblue'
14
+ rescue Exception
15
+ end
16
+
17
+ spec = Gem::Specification.new do |s|
18
+ s.name = "tiny_ds"
19
+ s.version = TinyDS::VERSION
20
+ s.platform = Gem::Platform::RUBY
21
+ s.has_rdoc = true
22
+ s.extra_rdoc_files = ["README.rdoc", "LICENSE"]
23
+ s.description = "Tiny datastore library for Google App Engine with JRuby"
24
+ s.summary = "Supports CRUD like a ActiveRecord or DataMapepr " +
25
+ "but with parent/child and entity-group-transaction"
26
+ s.author = "Takeru Sasaki"
27
+ s.email = "sasaki.takeru@gmail.com"
28
+ s.homepage = "http://github.com/takeru/tiny_ds"
29
+ s.require_path = 'lib'
30
+ s.files = %w(LICENSE README.rdoc Rakefile) +
31
+ Dir.glob("spec/**/*") + Dir.glob("lib/**/*")
32
+ s.add_dependency('appengine-apis')
33
+ end
34
+
35
+ task :default => :gem
36
+
37
+ Rake::GemPackageTask.new(spec) do |pkg|
38
+ pkg.gem_spec = spec
39
+ end
40
+
41
+ Rake::RDocTask.new do |rd|
42
+ rd.main = "README.rdoc"
43
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
44
+ end
45
+
@@ -0,0 +1,290 @@
1
+ # TODO
2
+ # callback before/after
3
+ # validations
4
+ # associations
5
+ #x default value
6
+ #x property list
7
+ # property Key
8
+ # property others
9
+ # nil=>false
10
+ # index=>false setUnindexedProperty
11
+ # version
12
+ # schema_version
13
+ #x new_record?
14
+ # find_by_xxx
15
+ #x get_by_id(id) get_by_name(name) : root entity only!!
16
+ #x get_by_id(parent, id) get_by_name(parent, name) : ancestor+Kind => key get
17
+ #x Foo.query.filter(:key=>123)
18
+ # Foo.filter(:key=>123)
19
+ # logger
20
+ # logging low level API calls.
21
+
22
+ require "time"
23
+
24
+ module TinyDS
25
+ class Base
26
+ class << self; attr_accessor :_property_definitions; end
27
+ RESERVED_PROPERTY_NAME = [:id, :name, :key, :entity, :parent_key, :parent]
28
+ def self.property(pname, ptype, opts={})
29
+ pname = pname.to_sym
30
+ if RESERVED_PROPERTY_NAME.include?(pname)
31
+ raise "property name '#{pname}' is reserved."
32
+ end
33
+ property_definitions[pname] = PropertyDefinition.new(pname, ptype, opts)
34
+ end
35
+
36
+ def self.property_definitions
37
+ self._property_definitions ||= {}
38
+ end
39
+
40
+ def self.property_definition(name)
41
+ property_definitions[name.to_sym] or raise "unknown property='#{name}'"
42
+ end
43
+
44
+ def self.default_attrs
45
+ attrs = {}
46
+ property_definitions.each do |pname,pdef|
47
+ if pdef.has_default?
48
+ attrs[pname] = pdef.default_value
49
+ end
50
+ end
51
+ attrs
52
+ end
53
+
54
+ # kind-string of entity
55
+ def self.kind
56
+ name
57
+ end
58
+
59
+ #include ActiveModel::Naming
60
+ # def self.model_name # for form-builder
61
+ # @model_name ||= ::ActiveModel::Name.new(self, kind)
62
+ # end
63
+
64
+ # foo.key
65
+ def key
66
+ @entity.key
67
+ end
68
+ def id
69
+ key.id
70
+ end
71
+ def name
72
+ key.name
73
+ end
74
+
75
+ # foo.parent_key
76
+ def parent_key
77
+ @entity.parent
78
+ end
79
+
80
+ def self.to_key(m)
81
+ case m
82
+ when AppEngine::Datastore::Key
83
+ m
84
+ when AppEngine::Datastore::Entity
85
+ m.key
86
+ when String
87
+ LowDS::KeyFactory.stringToKey(m)
88
+ else
89
+ if m.kind_of?(Base)
90
+ m.key
91
+ else
92
+ raise "unknown key type #{m.class}/#{m.inspect}"
93
+ end
94
+ end
95
+ end
96
+
97
+ # Foo.create({:title=>"hello",...}, :parent=>aaa, :id=>bbb, :name=>ccc, :key=>...)
98
+ def self.create(attrs={}, opts={})
99
+ m = new(attrs, opts)
100
+ m.save
101
+ m
102
+ end
103
+
104
+ # Foo.new
105
+ def initialize(attrs={}, opts={})
106
+ @entity = opts.delete(:entity)
107
+ unless @entity
108
+ if opts[:parent] && opts[:parent].kind_of?(Base)
109
+ opts = opts.dup
110
+ opts[:parent] = opts[:parent].entity
111
+ end
112
+ @entity = LowDS.build(self.class.kind, {}, opts)
113
+ self.attributes = self.class.default_attrs.merge(attrs || {})
114
+ @new_record = true
115
+ else
116
+ @new_record = false
117
+ end
118
+ end
119
+
120
+ def self.new_from_entity(_entity)
121
+ new(nil, :entity=>_entity)
122
+ end
123
+ attr_reader :entity
124
+
125
+ def new_record?
126
+ @new_record
127
+ end
128
+
129
+ # foo.save
130
+ def save
131
+ do_save
132
+ true
133
+ end
134
+
135
+ def do_save
136
+ __before_save_set_timestamps
137
+ # if @new_record && @entity.key && parent
138
+ # TinyDS.tx{
139
+ # if LowDS.get(@entity.key)
140
+ # raise KeyIsAlreadyTaken
141
+ # end
142
+ # LowDS.save(@entity)
143
+ # }
144
+ # else
145
+ LowDS.save(@entity)
146
+ # end
147
+ @new_record = false
148
+ nil
149
+ end
150
+ # class KeyIsAlreadyTaken < StandardError
151
+ # end
152
+
153
+ def __before_save_set_timestamps
154
+ if self.class.property_definitions[:created_at] && new_record?
155
+ self.created_at = Time.now
156
+ end
157
+ if self.class.property_definitions[:updated_at]
158
+ self.updated_at = Time.now
159
+ end
160
+ end
161
+
162
+ # Foo.get(key)
163
+ def self.get!(key)
164
+ self.new_from_entity(LowDS.get(key, :kind=>self.kind))
165
+ end
166
+ def self.get(key)
167
+ get!(key)
168
+ rescue AppEngine::Datastore::EntityNotFound => e
169
+ nil
170
+ end
171
+
172
+ def self._get_by_id_or_name!(id_or_name, parent)
173
+ key = if parent
174
+ parent = to_key(parent)
175
+ kfb = LowDS::KeyFactory::Builder.new(parent)
176
+ kfb.addChild(kind, id_or_name)
177
+ kfb.key
178
+ else
179
+ LowDS::KeyFactory::Builder.new(kind, id_or_name).key
180
+ end
181
+ get!(key)
182
+ end
183
+
184
+ def self.get_by_id!(id, parent=nil)
185
+ if id.kind_of?(String) && id==id.to_i.to_s
186
+ id = id.to_i
187
+ end
188
+ raise "id is not Integer" unless id.kind_of?(Integer)
189
+ _get_by_id_or_name!(id, parent)
190
+ end
191
+ def self.get_by_name!(name, parent=nil)
192
+ raise "id is not String" unless name.kind_of?(String)
193
+ _get_by_id_or_name!(name, parent)
194
+ end
195
+ def self.get_by_id(id, parent=nil)
196
+ if id.kind_of?(String) && id==id.to_i.to_s
197
+ id = id.to_i
198
+ end
199
+ _get_by_id_or_name!(id, parent)
200
+ rescue AppEngine::Datastore::EntityNotFound => e
201
+ nil
202
+ end
203
+ def self.get_by_name(name, parent=nil)
204
+ _get_by_id_or_name!(name, parent)
205
+ rescue AppEngine::Datastore::EntityNotFound => e
206
+ nil
207
+ end
208
+
209
+ # # Foo.find
210
+ # def self.find(*args)
211
+ # raise "todo"
212
+ # direction = dire==:desc ? AppEngine::Datastore::Query::DESCENDING : AppEngine::Datastore::Query::ASCENDING
213
+ # AppEngine::Datastore::Query.new("TxSrc").
214
+ # filter(:tx_kind, AppEngine::Datastore::Query::EQUAL, tx_kind).
215
+ # filter(:status, AppEngine::Datastore::Query::EQUAL, status).
216
+ # sort(:created_at, direction)
217
+ # end
218
+
219
+ def self.query
220
+ Query.new(self)
221
+ end
222
+
223
+ def self.count
224
+ query.count
225
+ end
226
+
227
+ # foo.destroy
228
+ def destroy
229
+ self.class.destroy(self)
230
+ end
231
+
232
+ # Foo.destroy([model, entity, key, ...])
233
+ def self.destroy(array)
234
+ array = [array] unless array.kind_of?(Array)
235
+ keys = array.collect do |m|
236
+ to_key(m)
237
+ end
238
+ AppEngine::Datastore.delete(keys)
239
+ end
240
+ def self.destroy_all
241
+ destroy(query.keys)
242
+ end
243
+
244
+ # set attributes
245
+ def attributes=(attrs)
246
+ attrs.each do |k,v|
247
+ set_property(k, v)
248
+ end
249
+ nil
250
+ end
251
+
252
+ # set property-value into @entity
253
+ def set_property(k,v)
254
+ prop_def = self.class.property_definition(k)
255
+ ds_v = prop_def.to_ds_value(v)
256
+ if ds_v.nil?
257
+ @entity.removeProperty(k)
258
+ else
259
+ @entity[k] = ds_v
260
+ end
261
+ # todo cache value read/write
262
+ end
263
+
264
+ # get property-value from @entity
265
+ def get_property(k)
266
+ prop_def = self.class.property_definition(k)
267
+ prop_def.to_ruby_value(@entity[k])
268
+ end
269
+
270
+ def method_missing(m, *args)
271
+ k, is_set = if m.to_s =~ /(.+)=$/
272
+ [$1.to_sym, true]
273
+ else
274
+ [m.to_sym, false]
275
+ end
276
+ if prop_def = self.class.property_definitions[k]
277
+ # TODO define method.
278
+ if is_set
279
+ raise if args.size!=1
280
+ set_property(k, args.first)
281
+ else
282
+ raise if args.size!=0
283
+ get_property(k)
284
+ end
285
+ else
286
+ super(m, *args)
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,115 @@
1
+ =begin
2
+ special pname
3
+ - key
4
+ - parent
5
+ - id
6
+ - name
7
+ =end
8
+
9
+
10
+ class ModelBase
11
+ @@properties = {}
12
+ def self.property(pname, ptype, opts={})
13
+ pname = pname.to_sym
14
+ if @@properties[pname]
15
+ raise "duplicated pname=#{pname}"
16
+ end
17
+ @@properties[pname] = Property.new(pname, ptype, opts)
18
+ end
19
+
20
+ # kind-string of entity
21
+ def self.kind
22
+ name
23
+ end
24
+
25
+ def self.tx(retries=0, &block)
26
+ transaction{
27
+ yield(block)
28
+ }
29
+ end
30
+
31
+ # Foo.create(attrs
32
+ def self.create(attrs={})
33
+ m = new(attrs)
34
+ m.save
35
+ m
36
+ end
37
+
38
+ # Foo.new
39
+ def initialize(attrs={})
40
+ attributes = attrs
41
+ end
42
+
43
+ def self.new_from_entity(entity)
44
+ m = new()
45
+ end
46
+
47
+ # foo.save
48
+ def save
49
+ _save
50
+ end
51
+
52
+ def _save
53
+ end
54
+
55
+ # Foo.get(key)
56
+ def self.get(key)
57
+ if key.kind_of?(String)
58
+ key = LowDS::KeyFactory.stringToKey(key)
59
+ end
60
+ raise "invalid key=#{key}" unless key.kind_of?(AppEngine::Datastore::Key)
61
+ self.new_from_entity(LowDS.get(key, :kind=>self.kind))
62
+ end
63
+
64
+ def self.find(*args)
65
+
66
+ end
67
+
68
+ # foo.destroy
69
+ def destroy
70
+ end
71
+
72
+ def attributes=(attrs)
73
+ # TODO key, parent, id, name
74
+
75
+ attrs.each do |k,v|
76
+ prop = @@properties[k.to_sym]
77
+ prop.value = v
78
+ end
79
+ nil
80
+ end
81
+
82
+ def method_missing(m, *args)
83
+ prop, is_set = if m =~ /(.+)=$/
84
+ [@@properties[$1.to_sym], true]
85
+ else
86
+ [@@properties[m.to_sym], false]
87
+ end
88
+ if prop
89
+ # TODO define method.
90
+ if is_set
91
+ raise if args.size!=1
92
+ prop.value = args.first
93
+ else
94
+ raise if args.size!=0
95
+ prop.value
96
+ end
97
+ else
98
+ super(m, *args)
99
+ end
100
+ end
101
+
102
+ class Property
103
+ def initialize(pname, ptype, opts)
104
+ @pname = pname
105
+ @ptype = ptype
106
+ @opts = opts
107
+ end
108
+ def value=(v)
109
+ # check_type or cast
110
+ @value = v
111
+ end
112
+ attr_reader :value
113
+ end
114
+ end
115
+
@@ -0,0 +1,161 @@
1
+ # memo デプロイ時に未完了のトランザクションがあってトランザクション定義がかわったら?version???
2
+ module TinyDS
3
+ class BaseTx
4
+ def src_phase(src, args)
5
+ raise "no impl."
6
+ end
7
+ def dest_phase(dest, args)
8
+ raise "no impl."
9
+ end
10
+ def self.tx_kind
11
+ name
12
+ end
13
+ def roll_forward_retries_limit
14
+ 100
15
+ end
16
+ def tx_retries
17
+ 3
18
+ end
19
+ attr_reader :tx_key
20
+
21
+ # トランザクション実行
22
+ # : src_phase dest_phase
23
+ # raised : failed ----
24
+ # false : OK failed
25
+ # true : OK OK
26
+ def self.exec(src, dest, args={})
27
+ tx = new
28
+ tx.create_tx(src, dest, args)
29
+ begin
30
+ tx.roll_forward
31
+ rescue AppEngine::Datastore::TransactionFailed => e
32
+ return false
33
+ end
34
+ return true
35
+ end
36
+
37
+ def self.roll_forward_all(limit=50)
38
+ pending_tx_query.each(:limit=>limit) do |tx_src|
39
+ tx = new
40
+ tx.restore_tx(tx_src)
41
+ begin
42
+ tx.roll_forward
43
+ rescue => e
44
+ #p "roll_forward failed. tx=[#{tx.tx_key}] e=[#{e.inspect}]"
45
+ end
46
+ end
47
+ nil
48
+ end
49
+
50
+ def self.pending_tx_query(status="pending", dire=:asc) # pending/done/failed
51
+ TxSrc.query.
52
+ filter(:tx_kind, "==", tx_kind).
53
+ filter(:status, "==", status).
54
+ sort(:created_at, dire)
55
+ end
56
+
57
+ # doneなTxSrcと対応するTxDoneを削除する
58
+ # def self.delete_done_tx
59
+ # end
60
+
61
+ class TxSrc < Base
62
+ property :tx_kind, :string
63
+ property :dest_key, :string
64
+ property :status, :string, :default=>"pending"
65
+ property :roll_forward_failed_count, :integer, :default=>0
66
+ property :args, :text
67
+ property :created_at, :time
68
+ property :done_at, :time
69
+ end
70
+
71
+ # トランザクション前半
72
+ # TODO TxIDを指定できるようにする。TxSrc#key.nameにTxIDを指定して重複実行防止
73
+ def create_tx(src, dest, args)
74
+ tx_src = nil
75
+ TinyDS.tx(tx_retries){
76
+ src = src.class.get(src.key)
77
+ src_phase(src, args)
78
+ src.save!
79
+
80
+ attrs = {
81
+ :tx_kind => self.class.tx_kind,
82
+ :dest_key => dest.key.to_s,
83
+ :args => args.to_yaml,
84
+ }
85
+ tx_src = TxSrc.create!(attrs, :parent=>src) # srcがparent, tx_srcがchild
86
+ # COMMIT:「srcの処理(=src_phase)、TxSrc作成」
87
+ }
88
+ @tx_key = tx_src.key
89
+ nil
90
+ end
91
+
92
+ def restore_tx(tx_src)
93
+ @tx_key = tx_src.key
94
+ nil
95
+ end
96
+
97
+ class TxDone < Base
98
+ property :done_at, :time
99
+ end
100
+
101
+ # トランザクション後半
102
+ def roll_forward
103
+ tx_src = TxSrc.get(@tx_key)
104
+
105
+ TinyDS.tx(tx_retries){
106
+ dest_key = LowDS::KeyFactory.stringToKey(tx_src.dest_key)
107
+ dest = dest_key.kind.constantize.get(dest_key)
108
+ done_name = "TxDone_#{@tx_key.to_s}"
109
+ done_key = LowDS::KeyFactory.createKey(dest.key, TxDone.kind, done_name)
110
+ begin
111
+ TxDone.get!(done_key)
112
+ # なにもしない : TxDoneが存在しているということはdest_phaseは処理済み
113
+ rescue AppEngine::Datastore::EntityNotFound => e
114
+ # TxDoneが無い→dest_phaseが未実行
115
+ attrs = {:done_at=>Time.now}
116
+ tx_done = TxDone.create!(attrs, :parent=>dest, :name=>done_name)
117
+ dest_phase(dest, YAML.load(tx_src.args)) # destの処理を実行
118
+ dest.save!
119
+ end
120
+ # memo: done_keyが同じTxDoneをcommitしようとするとTransactionFailedになるはず→dest_phaseもキャンセル
121
+ # COMMIT:「destの処理(=dest_phase)、TxDone作成」
122
+ }
123
+
124
+ # TxSrc#statusをdoneに
125
+ TinyDS.tx(tx_retries){
126
+ tx_src = TxSrc.get!(@tx_key)
127
+ if tx_src.status=="pending"
128
+ tx_src.status = "done"
129
+ tx_src.done_at = Time.now
130
+ tx_src.save!
131
+ end
132
+ }
133
+ return true
134
+ rescue => e
135
+ puts e.inspect
136
+ TinyDS.tx(tx_retries){
137
+ tx_src = TxSrc.get!(@tx_key)
138
+ tx_src.roll_forward_failed_count += 1
139
+ if roll_forward_retries_limit < tx_src.roll_forward_failed_count
140
+ tx_src.status = "failed"
141
+ end
142
+ tx_src.save!
143
+ }
144
+ return false
145
+ end
146
+
147
+ if false
148
+ require "benchmark"
149
+ def create_tx(src, dest, args)
150
+ RAILS_DEFAULT_LOGGER.info ["create_tx", Benchmark.measure{
151
+ _create_tx(src, dest, args)
152
+ }].inspect
153
+ end
154
+ def roll_forward
155
+ RAILS_DEFAULT_LOGGER.info ["roll_forward", Benchmark.measure{
156
+ _roll_forward
157
+ }].inspect
158
+ end
159
+ end
160
+ end
161
+ end