ndb-rails 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 407a37e97a7c0f50f39d4ca365e59bd9ecec788a9fe357e4ff4fbc61add79962
4
- data.tar.gz: 0de5474707d6bf68b55d1a44f56b612a8d5a59bc24cd7e96ff8066af00e98f8b
3
+ metadata.gz: 93088e1e11be89558d972761a9e2a7a565d617f165b90361e732240ee92c9361
4
+ data.tar.gz: e9e5eca9058d1bfa6ea840c4cc8fb17c734c69d5373b933f48b1d3dfb968ebfd
5
5
  SHA512:
6
- metadata.gz: 5eeed45c9f972b596dd588122600f1f7729ca07957ca771a8af0d0ca34085aa8b3c8e76d898a9605e034a6402c8370bdf57e209b2da5320a84d5904176fbeb50
7
- data.tar.gz: d52fa0af1af45659950dd152a68a5561aa311b98c7b3296f70df1185d6071b70740eae7f95763bb58d1aafc35984a6aa11cf04b822d05ab98ed2a02de208a88b
6
+ metadata.gz: c509825a020a9efff7a220a3fbbb842bde284f32c66d66368948234de6aa258585a0cac17b4da57feb1ff44b123fbf5c8d6cde8f998978f51aa406500cbb10ee
7
+ data.tar.gz: 853957c1575a0653c04f91bd8f16efb65415cd42656ab5a9de44f39360820d434d63d2df2a54301ec1964805c4b286dcdde969f04fefe54b79ebd5b6784452ed
data/README.md CHANGED
@@ -132,6 +132,32 @@ rescue Ndb::StaleObjectError
132
132
  end
133
133
  ```
134
134
 
135
+ ## Expiry (TTL like Redis)
136
+
137
+ Give the model an `expires: true` attribute. The daemon sets it for you.
138
+
139
+ ```ruby
140
+ class Session < Ndb::Record
141
+ primary_key :sid
142
+
143
+ attribute :user, :string
144
+ attribute :expires_at, :datetime, expires: true
145
+ end
146
+
147
+ Session.create!({ sid: "s1", user: "u" }, expire_in: 1.hour) # also create, save, update (seconds, a Duration or "30m")
148
+ session = Session.find("s1")
149
+
150
+ session.expire(30.minutes) # EXPIRE: sets expires_at in the object, other unsaved changes stay
151
+ session.ttl # => 1799, -1 (never expires), or nil (missing or expired)
152
+ session.persist_ttl! # PERSIST: removes the expiry
153
+ ```
154
+
155
+ - `persist_ttl!` is not called `persist!`: ActiveModel already uses `persisted?` for something else.
156
+ - `expire_in:` on `create`, `save` and `update` goes in the same write. `expire_in` wins over the `expires_at` value. After a write with `expire_in`, load the record again (`Session.find`) to see `expires_at`.
157
+ - `expire` and `persist_ttl!` raise `Ndb::NotFound` for a missing or expired document, and `Ndb::TtlNotEnabled` when the model has no `expires: true` attribute.
158
+ - The class-level `ttl 24.hours` stays: every document expires that long after its time field. `expire` and `persist_ttl!` raise `Ndb::TtlDerived` for it.
159
+ - Disk space of expired documents returns after compaction, and fully only with `wal_retention`; see [Freeing disk](https://github.com/thadeu/ndb/blob/main/docs/dev/configuration.md#freeing-disk).
160
+
135
161
  ## Query
136
162
 
137
163
  Relations are lazy and immutable. The request goes out when you call `to_a`, `first`, `each`, `count` and so on.
@@ -312,6 +338,7 @@ The transport and daemon errors are defined by the gem `ndb`. They all live in t
312
338
  | `InvalidCursor` | `cursor_invalid`, `cursor_mismatch`, `cursor_expired` |
313
339
  | `WatchLagged` | `watch_lagged` |
314
340
  | `HistoryTruncated` | `history_truncated` |
341
+ | `TtlNotEnabled`, `TtlDerived` | `ttl_not_enabled`, `ttl_derived` |
315
342
  | `RecordInvalid` | ActiveModel validation failed (has `record`) |
316
343
  | `RecordNotSaved` | `save!` returned false without errors |
317
344
  | `ConnectionError` | cannot reach the daemon. Subclasses: `ConnectError` (nothing was sent), `TimeoutError` |
@@ -12,24 +12,28 @@ module Ndb
12
12
  end
13
13
 
14
14
  class_methods do
15
- def create(attrs = {}, &block)
16
- return attrs.map { |a| create(a, &block) } if attrs.is_a?(Array)
15
+ def create(attrs = {}, expire_in: nil, **rest, &block)
16
+ return attrs.map { |a| create(a, expire_in: expire_in, &block) } if attrs.is_a?(Array)
17
+
18
+ attrs = attrs.merge(rest)
17
19
 
18
20
  record = new(attrs)
19
21
 
20
22
  yield record if block
21
- record.save
23
+ record.save(expire_in: expire_in)
22
24
 
23
25
  record
24
26
  end
25
27
 
26
- def create!(attrs = {}, &block)
27
- return attrs.map { |a| create!(a, &block) } if attrs.is_a?(Array)
28
+ def create!(attrs = {}, expire_in: nil, **rest, &block)
29
+ return attrs.map { |a| create!(a, expire_in: expire_in, &block) } if attrs.is_a?(Array)
30
+
31
+ attrs = attrs.merge(rest)
28
32
 
29
33
  record = new(attrs)
30
34
 
31
35
  yield record if block
32
- record.save!
36
+ record.save!(expire_in: expire_in)
33
37
 
34
38
  record
35
39
  end
@@ -71,26 +75,57 @@ module Ndb
71
75
  persisted? ? [id] : nil
72
76
  end
73
77
 
74
- def save(validate: true)
78
+ # expire_in (seconds, an ActiveSupport::Duration or "30m") makes the daemon
79
+ # set the expires field to now + expire_in in the same write. The model
80
+ # needs an `expires: true` attribute (Ndb::TtlNotEnabled otherwise).
81
+ def save(validate: true, expire_in: nil)
75
82
  return false if validate && !valid?(new_record? ? :create : :update)
76
83
 
84
+ @expire_in = wire_duration(expire_in)
85
+
77
86
  run_callbacks(:save) { new_record? ? create_record : update_record } ? true : false
87
+ ensure
88
+ @expire_in = nil
78
89
  end
79
90
 
80
91
  def save!(**)
81
92
  save(**) || raise(errors.any? ? RecordInvalid.new(self) : RecordNotSaved.new("Failed to save the record"))
82
93
  end
83
94
 
84
- def update(attrs)
85
- assign_attributes(attrs)
95
+ def update(attrs = {}, expire_in: nil, **rest)
96
+ assign_attributes(attrs.merge(rest))
97
+
98
+ save(expire_in: expire_in)
99
+ end
100
+
101
+ def update!(attrs = {}, expire_in: nil, **rest)
102
+ assign_attributes(attrs.merge(rest))
103
+
104
+ save!(expire_in: expire_in)
105
+ end
106
+
107
+ # Makes the stored document expire in a duration (like EXPIRE in Redis):
108
+ # `session.expire(30.minutes)`. The version and the expires attribute are
109
+ # refreshed; other unsaved changes stay unsaved. Raises Ndb::NotFound for
110
+ # a missing or expired document and Ndb::TtlNotEnabled without an
111
+ # `expires: true` attribute.
112
+ def expire(duration)
113
+ result = Rails.client.expire(self.class.collection_name, id, in: wire_duration(duration))
86
114
 
87
- save
115
+ refresh_expiry(result, Time.iso8601(result["expires_at"]))
88
116
  end
89
117
 
90
- def update!(attrs)
91
- assign_attributes(attrs)
118
+ # Removes the expiry of the stored document (PERSIST in Redis). It is not
119
+ # called `persist!` or `persisted?`, which mean something else in
120
+ # ActiveModel.
121
+ def persist_ttl!
122
+ refresh_expiry(Rails.client.persist(self.class.collection_name, id), nil)
123
+ end
92
124
 
93
- save!
125
+ # The seconds left (Integer), -1 when the document never expires, nil when
126
+ # it is missing or expired.
127
+ def ttl
128
+ Rails.client.ttl(self.class.collection_name, id)
94
129
  end
95
130
 
96
131
  def destroy
@@ -108,6 +143,23 @@ module Ndb
108
143
 
109
144
  private
110
145
 
146
+ def wire_duration(value)
147
+ value.is_a?(ActiveSupport::Duration) ? value.to_i : value
148
+ end
149
+
150
+ def refresh_expiry(result, expires_at)
151
+ @version = result["v"]
152
+
153
+ field = self.class.expires_field
154
+
155
+ return self unless field
156
+
157
+ public_send("#{field}=", expires_at)
158
+ clear_attribute_changes([field])
159
+
160
+ self
161
+ end
162
+
111
163
  def loaded!(version)
112
164
  @persisted = true
113
165
  @version = version
@@ -117,7 +169,7 @@ module Ndb
117
169
 
118
170
  def create_record
119
171
  run_callbacks(:create) do
120
- result = self.class.ndb_insert(insert_document)
172
+ result = self.class.ndb_insert(insert_document, expire_in: @expire_in)
121
173
 
122
174
  self.id = result["id"] if id.nil?
123
175
  loaded!(result["v"])
@@ -130,8 +182,8 @@ module Ndb
130
182
  run_callbacks(:update) do
131
183
  set, unset = changed_fields
132
184
 
133
- unless set.empty? && unset.empty?
134
- result = self.class.ndb_update(id, set: set, unset: unset, if_version: version)
185
+ unless set.empty? && unset.empty? && @expire_in.nil?
186
+ result = self.class.ndb_update(id, set: set, unset: unset, if_version: version, expire_in: @expire_in)
135
187
 
136
188
  @version = result["v"]
137
189
  end
@@ -137,10 +137,10 @@ module Ndb
137
137
  Rails.client.aggregate(collection_name, body)
138
138
  end
139
139
 
140
- def ndb_insert(doc)
140
+ def ndb_insert(doc, expire_in: nil)
141
141
  ensure_collection!
142
142
 
143
- Rails.client.insert(collection_name, [doc]).first
143
+ Rails.client.insert(collection_name, [doc], expire_in: expire_in).first
144
144
  end
145
145
 
146
146
  def ndb_update(id, **change)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ndb
4
4
  module Rails
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ndb-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - thadeu