activemodel-datastore 0.8.0 → 0.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +128 -128
- data/lib/active_model/datastore/carrier_wave_uploader.rb +3 -1
- data/lib/active_model/datastore/nested_attr.rb +3 -3
- data/lib/active_model/datastore/track_changes.rb +1 -1
- data/lib/active_model/datastore/version.rb +1 -1
- data/lib/active_model/datastore.rb +56 -14
- metadata +6 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62ccc081e2437c2c1ca324b12a78f888cb14bf5fb0d6cabe8ebee0c1089f3807
|
|
4
|
+
data.tar.gz: 5def43f01da73999c34f0f2a6bc66ead4249eda5df08281dd552da42856b0fea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 03f469254125d0dc7184395103cdea93648f0551e5a1289721ec5fe2dc382d0ccdf906087734875cfc8e79c815f51be766ab917d76cb2fdd9cde69e5858e1308
|
|
7
|
+
data.tar.gz: 71473fab63f1e02a4472db2a3c828492d1a23fddde05ca45aa275e9037a63cf252dc53140e93ac9bf5bc354a5fa38d549fec5d2c64565cb9f7fc0e7bfd05c896
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
### 0.10.0 / 2026-09-22
|
|
2
|
+
* stop retrying invalid argument errors
|
|
3
|
+
* make read retry counts configurable independently of writes
|
|
4
|
+
* log a warning when a retry chain gives up, including its attempt count
|
|
5
|
+
|
|
6
|
+
### 0.9.0 / 2026-08-24
|
|
7
|
+
* identify the Datastore operation, entity kind, and failed-attempt elapsed time in retry logs
|
|
8
|
+
* use a configurable retry logger that defaults to Rails.logger and falls back to standard output
|
|
9
|
+
* update the development environment to Ruby 3.4.10 and the example application to Rails 8.1
|
|
10
|
+
* test CarrierWave integration against CarrierWave 3.1
|
|
11
|
+
|
|
1
12
|
### 0.8.0 / 2024-03-05
|
|
2
13
|
* updating GCLOUD_KEYFILE_JSON to work with the latest version of GoogleAuth
|
|
3
14
|
|
data/README.md
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
Active Model Datastore
|
|
2
2
|
===================================
|
|
3
3
|
|
|
4
|
-
Makes the [google-cloud-datastore](https://
|
|
5
|
-
|
|
4
|
+
Makes the [google-cloud-datastore](https://cloud.google.com/ruby/docs/reference/google-cloud-datastore/latest)
|
|
5
|
+
gem compliant with [active_model](https://github.com/rails/rails/tree/master/activemodel)
|
|
6
|
+
conventions and compatible with your Rails 5+ applications.
|
|
6
7
|
|
|
7
|
-
Why would you want to use Google's NoSQL
|
|
8
|
-
with Rails?
|
|
8
|
+
Why would you want to use Google's NoSQL
|
|
9
|
+
[Firestore in Datastore mode](https://docs.cloud.google.com/datastore/docs) with Rails?
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Use it when you want a Rails app backed by a fully managed, massively scalable NoSQL database,
|
|
12
|
+
without provisioning database servers or manually sharding data. Datastore stores records as
|
|
13
|
+
entities with flexible properties, so your models do not require a fixed database schema. It
|
|
14
|
+
automatically handles scaling and replication, provides highly available and durable storage, and
|
|
15
|
+
supports indexed queries and ACID transactions.
|
|
14
16
|
|
|
15
17
|
[](https://badge.fury.io/rb/activemodel-datastore)
|
|
16
|
-
[](https://github.com/Agrimatics/activemodel-datastore/actions/workflows/ci.yml)
|
|
17
19
|
|
|
18
20
|
## Table of contents
|
|
19
21
|
|
|
@@ -21,9 +23,9 @@ suited for unstructured or semi-structured application data.
|
|
|
21
23
|
- [Model Example](#model)
|
|
22
24
|
- [Controller Example](#controller)
|
|
23
25
|
- [Retrieving Entities](#queries)
|
|
24
|
-
- [Datastore Consistency](#consistency)
|
|
26
|
+
- [Datastore Consistency and Concurrency](#consistency)
|
|
25
27
|
- [Datastore Indexes](#indexes)
|
|
26
|
-
- [
|
|
28
|
+
- [Firestore Emulator](#emulator)
|
|
27
29
|
- [Example Rails App](#rails)
|
|
28
30
|
- [CarrierWave File Uploads](#carrierwave)
|
|
29
31
|
- [Track Changes](#track_changes)
|
|
@@ -48,12 +50,13 @@ gem 'activemodel-datastore'
|
|
|
48
50
|
|
|
49
51
|
Create a Google Cloud account [here](https://cloud.google.com) and create a project.
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Google Cloud
|
|
55
|
-
credentials
|
|
56
|
-
|
|
53
|
+
Follow the [activation instructions](https://cloud.google.com/datastore/docs/activate) to create a
|
|
54
|
+
Firestore in Datastore mode database for the project.
|
|
55
|
+
|
|
56
|
+
The Google Cloud client libraries use Application Default Credentials (ADC). On Google Cloud,
|
|
57
|
+
credentials are discovered automatically from the service account attached to the application. Grant
|
|
58
|
+
that service account only the access it needs; `roles/datastore.user` provides read/write access to
|
|
59
|
+
Datastore data.
|
|
57
60
|
|
|
58
61
|
Set your project id in an `ENV` variable named `GCLOUD_PROJECT`.
|
|
59
62
|
|
|
@@ -63,22 +66,39 @@ To locate your project ID:
|
|
|
63
66
|
2. From the projects list, select the name of your project.
|
|
64
67
|
3. On the left, click Dashboard. The project name and ID are displayed in the Dashboard.
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
For applications outside Google Cloud, configure ADC for the hosting environment. If a service account
|
|
70
|
+
key is required, the Ruby client supports `GOOGLE_APPLICATION_CREDENTIALS` with the path to its JSON
|
|
71
|
+
file. Active Model Datastore also supports the following environment variables for platforms where
|
|
72
|
+
the JSON must be stored directly in environment variables:
|
|
70
73
|
|
|
71
74
|
```bash
|
|
72
|
-
SERVICE_ACCOUNT_PRIVATE_KEY
|
|
73
|
-
SERVICE_ACCOUNT_CLIENT_EMAIL
|
|
75
|
+
export SERVICE_ACCOUNT_PRIVATE_KEY='-----BEGIN PRIVATE KEY-----\nMIIFfb3...5dmFtABy\n-----END PRIVATE KEY-----\n'
|
|
76
|
+
export SERVICE_ACCOUNT_CLIENT_EMAIL='web-app@app-name.iam.gserviceaccount.com'
|
|
74
77
|
```
|
|
75
78
|
|
|
76
|
-
On Heroku the
|
|
79
|
+
On Heroku the environment variables can be set under **Settings > Config Vars**.
|
|
77
80
|
|
|
78
81
|
Active Model Datastore will then handle the authentication for you, and the datastore instance can
|
|
79
82
|
be accessed with `CloudDatastore.dataset`.
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
Datastore retries use `Rails.logger` automatically when Rails is available, which preserves tagged
|
|
85
|
+
logging such as request IDs. Other applications fall back to standard output. To use another logger,
|
|
86
|
+
configure it during application initialization:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
ActiveModel::Datastore.logger = MyApplication.logger
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
To change the gem's read retry count (default: `5`), configure it during initialization:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
ActiveModel::Datastore.read_retry_count = 2
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Set it to `0` to disable read retries. Write retry counts are unchanged. Invalid argument errors
|
|
99
|
+
are never retried.
|
|
100
|
+
|
|
101
|
+
There is an example Puma config file [here](https://github.com/Agrimatics/activemodel-datastore/blob/main/test/support/datastore_example_rails_app/config/puma.rb).
|
|
82
102
|
|
|
83
103
|
## <a name="model"></a>Model Example
|
|
84
104
|
|
|
@@ -96,18 +116,18 @@ class User
|
|
|
96
116
|
end
|
|
97
117
|
```
|
|
98
118
|
|
|
99
|
-
Data objects in
|
|
119
|
+
Data objects in Datastore are known as entities. Entities are of a kind. An entity has one
|
|
100
120
|
or more named properties, each of which can have one or more values. Think of them like this:
|
|
101
121
|
* 'Kind' (which is your table and the name of your Rails model)
|
|
102
122
|
* 'Entity' (which is the record from the table)
|
|
103
123
|
* 'Property' (which is the attribute of the record)
|
|
104
124
|
|
|
105
|
-
The `entity_properties` method defines an Array of properties that belong to the entity in
|
|
106
|
-
|
|
125
|
+
The `entity_properties` method defines an Array of properties that belong to the entity in
|
|
126
|
+
Datastore. Define the attributes of your model using `attr_accessor`. With this approach, Rails
|
|
107
127
|
deals solely with ActiveModel objects. The objects are converted to/from entities automatically
|
|
108
128
|
during save/query operations. You can still use virtual attributes on the model (such as the
|
|
109
129
|
`:state` attribute above) by simply excluding it from `entity_properties`. In this example state
|
|
110
|
-
is available to the model but won't be persisted with the entity in
|
|
130
|
+
is available to the model but won't be persisted with the entity in Datastore.
|
|
111
131
|
|
|
112
132
|
Validations work as you would expect:
|
|
113
133
|
|
|
@@ -225,12 +245,12 @@ end
|
|
|
225
245
|
|
|
226
246
|
## <a name="queries"></a>Retrieving Entities
|
|
227
247
|
|
|
228
|
-
Each entity in
|
|
248
|
+
Each entity in Datastore has a key that uniquely identifies it. The key consists of the
|
|
229
249
|
following components:
|
|
230
250
|
|
|
231
251
|
* the kind of the entity, which is User in these examples
|
|
232
252
|
* an identifier for the individual entity, which can be either a a key name string or an integer numeric ID
|
|
233
|
-
* an optional ancestor path locating the entity within the
|
|
253
|
+
* an optional ancestor path locating the entity within the Datastore hierarchy
|
|
234
254
|
|
|
235
255
|
#### [all(options = {})](http://www.rubydoc.info/gems/activemodel-datastore/ActiveModel%2FDatastore%2FClassMethods:all)
|
|
236
256
|
Queries entities using the provided options. When a limit option is provided queries up to the limit
|
|
@@ -279,34 +299,34 @@ user = User.find_by(name: 'Joe')
|
|
|
279
299
|
user = User.find_by(name: 'Bryce', ancestor: parent_key)
|
|
280
300
|
```
|
|
281
301
|
|
|
282
|
-
|
|
283
|
-
work,
|
|
284
|
-
|
|
285
|
-
## <a name="consistency"></a>Datastore Consistency
|
|
302
|
+
Google documents how [Datastore queries](https://cloud.google.com/datastore/docs/concepts/queries)
|
|
303
|
+
work, including their [restrictions](https://cloud.google.com/datastore/docs/concepts/queries#restrictions_on_queries).
|
|
286
304
|
|
|
287
|
-
|
|
288
|
-
machines and uses synchronous replication over a wide geographic area. Because of this architecture
|
|
289
|
-
it offers a balance of strong and eventual consistency.
|
|
305
|
+
## <a name="consistency"></a>Datastore Consistency and Concurrency
|
|
290
306
|
|
|
291
|
-
|
|
307
|
+
Firestore in Datastore mode is strongly consistent by default. Queries and key lookups reflect
|
|
308
|
+
completed writes, including queries that do not use an ancestor. The legacy Cloud Datastore behavior
|
|
309
|
+
where a newly created entity might not immediately appear in a global query does not apply.
|
|
292
310
|
|
|
293
|
-
|
|
294
|
-
Eventual consistency is a theoretical guarantee that, provided no new updates to an entity are made,
|
|
295
|
-
all reads of the entity will eventually return the last updated value.
|
|
311
|
+
Datastore supports three concurrency modes that determine how concurrent transactions interact:
|
|
296
312
|
|
|
297
|
-
|
|
298
|
-
|
|
313
|
+
- `PESSIMISTIC` uses reader/writer locks and is the default for new databases.
|
|
314
|
+
- `OPTIMISTIC` allows concurrent transactions, but only the first conflicting transaction to commit
|
|
315
|
+
succeeds.
|
|
316
|
+
- `OPTIMISTIC_WITH_ENTITY_GROUPS` preserves legacy Cloud Datastore entity-group transaction
|
|
317
|
+
semantics. Transactions are limited to 25 entity groups, writes to an entity group are limited to
|
|
318
|
+
one per second, and queries within transactions must be ancestor queries.
|
|
299
319
|
|
|
300
|
-
|
|
320
|
+
Use the following command to inspect a database's concurrency mode:
|
|
301
321
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
322
|
+
```bash
|
|
323
|
+
gcloud firestore databases describe --project=PROJECT_ID --database=DATABASE_ID
|
|
324
|
+
```
|
|
305
325
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
326
|
+
Entity groups are hierarchies formed by a root entity and its children. They are useful for modeling
|
|
327
|
+
related entities and are required by the transactional restrictions of
|
|
328
|
+
`OPTIMISTIC_WITH_ENTITY_GROUPS`. To create an entity group, specify an ancestor path as part of the
|
|
329
|
+
child entity's key.
|
|
310
330
|
|
|
311
331
|
Before using the `save` method, assign the `parent_key_id` attribute an ID. Let's say that 12345
|
|
312
332
|
represents the ID of the company that the users belong to. The key of the user entity will now
|
|
@@ -314,14 +334,10 @@ look like this:
|
|
|
314
334
|
|
|
315
335
|
`@key=#<Google::Cloud::Datastore::Key @kind="User", @id=1, @parent=#<Google::Cloud::Datastore::Key @kind="ParentUser", @id=12345>>`
|
|
316
336
|
|
|
317
|
-
All of the User entities will now belong to an entity group named ParentUser and can be queried by the
|
|
337
|
+
All of the User entities will now belong to an entity group named ParentUser and can be queried by the
|
|
318
338
|
Company ID. When we query for the users we will provide User.parent_key(12345) as the ancestor option.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
However, there is a small downside. Entities with the same ancestor are limited to 1 write per second.
|
|
323
|
-
Also, the entity group relationship cannot be changed after creating the entity (as you can't modify
|
|
324
|
-
an entity's key after it has been saved).
|
|
339
|
+
The entity group relationship cannot be changed after creating the entity because an entity's key
|
|
340
|
+
cannot be modified after it has been saved.
|
|
325
341
|
|
|
326
342
|
The Users controller would now look like this:
|
|
327
343
|
|
|
@@ -384,24 +400,24 @@ class UsersController < ApplicationController
|
|
|
384
400
|
end
|
|
385
401
|
```
|
|
386
402
|
|
|
387
|
-
See
|
|
403
|
+
See the Datastore documentation for details about
|
|
404
|
+
[transactions, isolation, and concurrency modes](https://cloud.google.com/datastore/docs/concepts/transactions#concurrency_modes).
|
|
388
405
|
|
|
389
406
|
## <a name="indexes"></a>Datastore Indexes
|
|
390
407
|
|
|
391
|
-
Every
|
|
408
|
+
Every Datastore query computes its results using one or more indexes. The
|
|
392
409
|
indexes contain entity keys in a sequence specified by the index's properties and, optionally,
|
|
393
410
|
the entity's ancestors.
|
|
394
411
|
|
|
395
412
|
There are two types of indexes, *built-in* and *composite*.
|
|
396
413
|
|
|
397
414
|
#### Built-in
|
|
398
|
-
By default,
|
|
399
|
-
These single property indexes are suitable for simple types of queries.
|
|
400
|
-
do not count against your index limit.
|
|
415
|
+
By default, Datastore automatically predefines an index for each property of each entity kind.
|
|
416
|
+
These single property indexes are suitable for simple types of queries.
|
|
401
417
|
|
|
402
418
|
#### Composite
|
|
403
|
-
Composite
|
|
404
|
-
queries and are defined in an index.yaml file.
|
|
419
|
+
Composite indexes include multiple property values per indexed entity. Composite indexes support
|
|
420
|
+
complex queries and are defined in an `index.yaml` file.
|
|
405
421
|
|
|
406
422
|
Composite indexes are required for queries of the following form:
|
|
407
423
|
|
|
@@ -413,65 +429,50 @@ Composite indexes are required for queries of the following form:
|
|
|
413
429
|
|
|
414
430
|
*NOTE*: Inequality filters are LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL.
|
|
415
431
|
|
|
416
|
-
Google
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
Add the following line to your ~/.bash_profile:
|
|
445
|
-
|
|
446
|
-
export PATH="~/google-cloud-sdk/platform/cloud-datastore-emulator:$PATH"
|
|
447
|
-
|
|
448
|
-
Restart your shell:
|
|
449
|
-
|
|
450
|
-
exec -l $SHELL
|
|
451
|
-
|
|
452
|
-
To create the local development datastore execute the following from the root of the project:
|
|
453
|
-
|
|
454
|
-
$ cloud_datastore_emulator create tmp/local_datastore
|
|
455
|
-
|
|
456
|
-
To create the local test datastore execute the following from the root of the project:
|
|
457
|
-
|
|
458
|
-
$ cloud_datastore_emulator create tmp/test_datastore
|
|
459
|
-
|
|
460
|
-
To start the local Cloud Datastore emulator:
|
|
461
|
-
|
|
462
|
-
$ cloud_datastore_emulator start --port=8180 tmp/local_datastore
|
|
463
|
-
|
|
432
|
+
See Google's [Datastore index documentation](https://cloud.google.com/datastore/docs/concepts/indexes)
|
|
433
|
+
for more information.
|
|
434
|
+
|
|
435
|
+
## <a name="emulator"></a>Firestore Emulator
|
|
436
|
+
|
|
437
|
+
Install the [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) and the Firestore emulator:
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
gcloud components install cloud-firestore-emulator
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
The emulator requires Java 21 or later. Add the emulator executable to your shell's `PATH` because
|
|
444
|
+
the test helpers invoke it directly:
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
export PATH="$HOME/google-cloud-sdk/platform/cloud-firestore-emulator:$PATH"
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
The Firestore emulator runs in memory by default, so there is no datastore directory to create.
|
|
451
|
+
Start it in Datastore mode on the development port with:
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
cloud_firestore_emulator start --database-mode=datastore-mode --port=8180
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Set `DATASTORE_EMULATOR_HOST=localhost:8180` so the Ruby client connects to the emulator. The gem sets
|
|
458
|
+
this automatically for Rails development and uses port 8181 for Rails tests.
|
|
459
|
+
|
|
464
460
|
## <a name="rails"></a>Example Rails App
|
|
465
461
|
|
|
466
|
-
There is an example Rails
|
|
462
|
+
There is an example Rails 8.1 app in the test directory [here](https://github.com/Agrimatics/activemodel-datastore/tree/main/test/support/datastore_example_rails_app).
|
|
463
|
+
|
|
464
|
+
Start the Firestore emulator in Datastore mode in one terminal:
|
|
467
465
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
466
|
+
```bash
|
|
467
|
+
cloud_firestore_emulator start --database-mode=datastore-mode --port=8180
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Then start the example application in another terminal:
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
bundle install
|
|
474
|
+
rails server
|
|
475
|
+
```
|
|
475
476
|
|
|
476
477
|
Navigate to http://localhost:3000.
|
|
477
478
|
|
|
@@ -577,13 +578,12 @@ class Recipe
|
|
|
577
578
|
end
|
|
578
579
|
```
|
|
579
580
|
|
|
580
|
-
|
|
581
|
+
Alternatively, `:reject_if` also accepts a symbol naming a method:
|
|
581
582
|
|
|
582
583
|
```ruby
|
|
583
584
|
class Recipe
|
|
584
585
|
def ingredients_attributes=(attributes)
|
|
585
|
-
|
|
586
|
-
assign_nested_attributes(:ingredients, attributes, reject_if: reject_recipes)
|
|
586
|
+
assign_nested_attributes(:ingredients, attributes, reject_if: :reject_recipes)
|
|
587
587
|
end
|
|
588
588
|
|
|
589
589
|
def reject_recipes(attributes)
|
|
@@ -594,12 +594,12 @@ end
|
|
|
594
594
|
|
|
595
595
|
Within the parent model `valid?` will validate the parent and associated children and
|
|
596
596
|
`nested_models` will return the child objects. If the nested form submitted params contained
|
|
597
|
-
a truthy `_destroy` key, the appropriate
|
|
598
|
-
to
|
|
597
|
+
a truthy `_destroy` key, the appropriate nested models will have `marked_for_destruction` set
|
|
598
|
+
to `true`.
|
|
599
599
|
|
|
600
600
|
## <a name="gotchas"></a>Datastore Gotchas
|
|
601
601
|
#### Ordering of query results is undefined when no sort order is specified.
|
|
602
602
|
When a query does not specify a sort order, the results are returned in the order they are retrieved.
|
|
603
|
-
As
|
|
603
|
+
As the Datastore implementation evolves (or if a project's indexes change), this order may change.
|
|
604
604
|
Therefore, if your application requires its query results in a particular order, be sure to specify
|
|
605
605
|
that sort order explicitly in the query.
|
|
@@ -44,7 +44,9 @@ module CarrierWaveUploader
|
|
|
44
44
|
self.class.uploaders.keys.each do |attr_name|
|
|
45
45
|
entity[attr_name] = append_files(entity[attr_name], existing_files[attr_name])
|
|
46
46
|
end
|
|
47
|
-
self.class.retry_on_exception?
|
|
47
|
+
self.class.retry_on_exception?(operation: 'save') do
|
|
48
|
+
CloudDatastore.dataset.save entity
|
|
49
|
+
end
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -78,6 +78,8 @@ module ActiveModel::Datastore
|
|
|
78
78
|
extend ActiveSupport::Concern
|
|
79
79
|
include ActiveModel::Model
|
|
80
80
|
|
|
81
|
+
UNASSIGNABLE_KEYS = %w[id _destroy].freeze
|
|
82
|
+
|
|
81
83
|
included do
|
|
82
84
|
attr_accessor :nested_attributes, :marked_for_destruction, :_destroy
|
|
83
85
|
end
|
|
@@ -173,8 +175,6 @@ module ActiveModel::Datastore
|
|
|
173
175
|
|
|
174
176
|
private
|
|
175
177
|
|
|
176
|
-
UNASSIGNABLE_KEYS = %w[id _destroy].freeze
|
|
177
|
-
|
|
178
178
|
def validate_attributes(attributes)
|
|
179
179
|
attributes = attributes.to_h if attributes.respond_to?(:permitted?)
|
|
180
180
|
unless attributes.is_a?(Hash)
|
|
@@ -248,7 +248,7 @@ module ActiveModel::Datastore
|
|
|
248
248
|
def validate_each(record, attribute, value)
|
|
249
249
|
return unless Array(value).reject(&:valid?).any?
|
|
250
250
|
|
|
251
|
-
record.errors.add(attribute, :invalid, **options
|
|
251
|
+
record.errors.add(attribute, :invalid, **options, value: value)
|
|
252
252
|
end
|
|
253
253
|
end
|
|
254
254
|
end
|
|
@@ -71,7 +71,7 @@ module ActiveModel::Datastore
|
|
|
71
71
|
raise TrackChangesError, 'Object has not been configured for change tracking.'
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
changed = marked_for_destruction?
|
|
74
|
+
changed = marked_for_destruction? || false
|
|
75
75
|
tracked_attributes.each do |attr|
|
|
76
76
|
break if changed
|
|
77
77
|
|
|
@@ -119,8 +119,25 @@ module ActiveModel::Datastore
|
|
|
119
119
|
include ActiveModel::Datastore::PropertyValues
|
|
120
120
|
include ActiveModel::Datastore::TrackChanges
|
|
121
121
|
|
|
122
|
+
def self.logger
|
|
123
|
+
@logger || (Rails.logger if defined?(Rails) == 'constant')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.logger=(logger)
|
|
127
|
+
@logger = logger
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.read_retry_count
|
|
131
|
+
@read_retry_count || 5
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.read_retry_count=(count)
|
|
135
|
+
@read_retry_count = count
|
|
136
|
+
end
|
|
137
|
+
|
|
122
138
|
included do
|
|
123
|
-
private_class_method :query_options, :query_sort, :query_property_filter, :find_all_entities
|
|
139
|
+
private_class_method :query_options, :query_sort, :query_property_filter, :find_all_entities,
|
|
140
|
+
:retry_read, :log_retry_failure
|
|
124
141
|
define_model_callbacks :save, :update, :destroy
|
|
125
142
|
attr_accessor :id, :parent_key_id, :entity_property_values
|
|
126
143
|
end
|
|
@@ -183,7 +200,7 @@ module ActiveModel::Datastore
|
|
|
183
200
|
|
|
184
201
|
run_callbacks :update do
|
|
185
202
|
entity = build_entity
|
|
186
|
-
self.class.retry_on_exception? { CloudDatastore.dataset.save entity }
|
|
203
|
+
self.class.retry_on_exception?(operation: 'save') { CloudDatastore.dataset.save entity }
|
|
187
204
|
end
|
|
188
205
|
end
|
|
189
206
|
|
|
@@ -191,7 +208,7 @@ module ActiveModel::Datastore
|
|
|
191
208
|
run_callbacks :destroy do
|
|
192
209
|
key = CloudDatastore.dataset.key self.class.name, id
|
|
193
210
|
key.parent = self.class.parent_key(parent_key_id) if parent?
|
|
194
|
-
self.class.retry_on_exception? { CloudDatastore.dataset.delete key }
|
|
211
|
+
self.class.retry_on_exception?(operation: 'delete') { CloudDatastore.dataset.delete key }
|
|
195
212
|
end
|
|
196
213
|
end
|
|
197
214
|
|
|
@@ -202,7 +219,9 @@ module ActiveModel::Datastore
|
|
|
202
219
|
|
|
203
220
|
run_callbacks :save do
|
|
204
221
|
entity = build_entity(parent)
|
|
205
|
-
success = self.class.retry_on_exception?
|
|
222
|
+
success = self.class.retry_on_exception?(operation: 'save') do
|
|
223
|
+
CloudDatastore.dataset.save entity
|
|
224
|
+
end
|
|
206
225
|
self.id = entity.key.id if success
|
|
207
226
|
self.parent_key_id = entity.key.parent.id if entity.key.parent.present?
|
|
208
227
|
success
|
|
@@ -229,7 +248,7 @@ module ActiveModel::Datastore
|
|
|
229
248
|
def find_entity(id_or_name, parent = nil)
|
|
230
249
|
key = CloudDatastore.dataset.key name, id_or_name
|
|
231
250
|
key.parent = parent if parent.present?
|
|
232
|
-
|
|
251
|
+
retry_read('find') { CloudDatastore.dataset.find key }
|
|
233
252
|
end
|
|
234
253
|
|
|
235
254
|
##
|
|
@@ -290,7 +309,7 @@ module ActiveModel::Datastore
|
|
|
290
309
|
def all(options = {})
|
|
291
310
|
next_cursor = nil
|
|
292
311
|
query = build_query(options)
|
|
293
|
-
query_results =
|
|
312
|
+
query_results = retry_read('run query') { CloudDatastore.dataset.run query }
|
|
294
313
|
if options[:limit]
|
|
295
314
|
next_cursor = query_results.cursor if query_results.size == options[:limit]
|
|
296
315
|
return from_entities(query_results.all), next_cursor
|
|
@@ -342,7 +361,7 @@ module ActiveModel::Datastore
|
|
|
342
361
|
query.ancestor(args[:ancestor]) if args[:ancestor]
|
|
343
362
|
query.limit(1)
|
|
344
363
|
query.where(args.keys[0].to_s, '=', args.values[0])
|
|
345
|
-
query_results =
|
|
364
|
+
query_results = retry_read('run query') { CloudDatastore.dataset.run query }
|
|
346
365
|
from_entity(query_results.first)
|
|
347
366
|
end
|
|
348
367
|
|
|
@@ -400,15 +419,19 @@ module ActiveModel::Datastore
|
|
|
400
419
|
query_options(query, options)
|
|
401
420
|
end
|
|
402
421
|
|
|
403
|
-
def retry_on_exception?(max_retry_count = 5)
|
|
422
|
+
def retry_on_exception?(max_retry_count = 5, operation: nil, kind: name)
|
|
404
423
|
retries = 0
|
|
405
424
|
sleep_time = 0.25
|
|
406
425
|
begin
|
|
426
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) if operation
|
|
407
427
|
yield
|
|
408
428
|
rescue Google::Cloud::Error => e
|
|
409
|
-
|
|
429
|
+
giving_up = e.is_a?(Google::Cloud::InvalidArgumentError) || retries >= max_retry_count
|
|
430
|
+
ending = "retrying in #{sleep_time}#{' s' if operation}"
|
|
431
|
+
ending = "giving up after #{retries + 1} attempts" if giving_up
|
|
432
|
+
log_retry_failure(e, ending, operation: operation, kind: kind, started_at: started_at)
|
|
433
|
+
return false if giving_up
|
|
410
434
|
|
|
411
|
-
puts "\e[33mRescued exception #{e.message.inspect}, retrying in #{sleep_time}\e[0m"
|
|
412
435
|
# 0.25, 0.5, 1, 2, and 4 second between retries.
|
|
413
436
|
sleep sleep_time
|
|
414
437
|
retries += 1
|
|
@@ -417,15 +440,19 @@ module ActiveModel::Datastore
|
|
|
417
440
|
end
|
|
418
441
|
end
|
|
419
442
|
|
|
420
|
-
def retry_on_exception(max_retry_count = 5)
|
|
443
|
+
def retry_on_exception(max_retry_count = 5, operation: nil, kind: name)
|
|
421
444
|
retries = 0
|
|
422
445
|
sleep_time = 0.25
|
|
423
446
|
begin
|
|
447
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) if operation
|
|
424
448
|
yield
|
|
425
449
|
rescue Google::Cloud::Error => e
|
|
426
|
-
|
|
450
|
+
giving_up = e.is_a?(Google::Cloud::InvalidArgumentError) || retries >= max_retry_count
|
|
451
|
+
ending = "retrying in #{sleep_time}#{' s' if operation}"
|
|
452
|
+
ending = "giving up after #{retries + 1} attempts" if giving_up
|
|
453
|
+
log_retry_failure(e, ending, operation: operation, kind: kind, started_at: started_at)
|
|
454
|
+
raise if giving_up
|
|
427
455
|
|
|
428
|
-
puts "\e[33mRescued exception #{e.message.inspect}, retrying in #{sleep_time}\e[0m"
|
|
429
456
|
# 0.25, 0.5, 1, 2, and 4 second between retries.
|
|
430
457
|
sleep sleep_time
|
|
431
458
|
retries += 1
|
|
@@ -443,6 +470,21 @@ module ActiveModel::Datastore
|
|
|
443
470
|
|
|
444
471
|
# **************** private ****************
|
|
445
472
|
|
|
473
|
+
def log_retry_failure(error, ending, operation:, kind:, started_at:)
|
|
474
|
+
if operation
|
|
475
|
+
elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round
|
|
476
|
+
message = "\e[33mDatastore #{operation} failed for #{kind} after #{elapsed_ms} ms: " \
|
|
477
|
+
"#{error.message.inspect}; #{ending}\e[0m"
|
|
478
|
+
else
|
|
479
|
+
message = "\e[33mRescued exception #{error.message.inspect}, #{ending}\e[0m"
|
|
480
|
+
end
|
|
481
|
+
ActiveModel::Datastore.logger ? ActiveModel::Datastore.logger.warn(message) : puts(message)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def retry_read(operation, &block)
|
|
485
|
+
retry_on_exception(ActiveModel::Datastore.read_retry_count, operation: operation, &block)
|
|
486
|
+
end
|
|
487
|
+
|
|
446
488
|
def query_options(query, options)
|
|
447
489
|
query.ancestor(options[:ancestor]) if options[:ancestor]
|
|
448
490
|
query.cursor(options[:cursor]) if options[:cursor]
|
|
@@ -491,7 +533,7 @@ module ActiveModel::Datastore
|
|
|
491
533
|
def find_all_entities(ids_or_names, parent)
|
|
492
534
|
keys = ids_or_names.map { |id| CloudDatastore.dataset.key name, id }
|
|
493
535
|
keys.map { |key| key.parent = parent } if parent.present?
|
|
494
|
-
|
|
536
|
+
retry_read('find all') { CloudDatastore.dataset.find_all keys }
|
|
495
537
|
end
|
|
496
538
|
|
|
497
539
|
def build_model(entity)
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activemodel-datastore
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bryce McLean
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activemodel
|
|
@@ -148,14 +147,14 @@ dependencies:
|
|
|
148
147
|
requirements:
|
|
149
148
|
- - "~>"
|
|
150
149
|
- !ruby/object:Gem::Version
|
|
151
|
-
version: '
|
|
150
|
+
version: '3.1'
|
|
152
151
|
type: :development
|
|
153
152
|
prerelease: false
|
|
154
153
|
version_requirements: !ruby/object:Gem::Requirement
|
|
155
154
|
requirements:
|
|
156
155
|
- - "~>"
|
|
157
156
|
- !ruby/object:Gem::Version
|
|
158
|
-
version: '
|
|
157
|
+
version: '3.1'
|
|
159
158
|
description: Makes the google-cloud-datastore gem compliant with active_model conventions
|
|
160
159
|
and compatible with your Rails 5+ applications.
|
|
161
160
|
email:
|
|
@@ -182,10 +181,9 @@ licenses:
|
|
|
182
181
|
- MIT
|
|
183
182
|
metadata:
|
|
184
183
|
homepage_uri: https://github.com/Agrimatics/activemodel-datastore
|
|
185
|
-
changelog_uri: https://github.com/Agrimatics/activemodel-datastore/blob/
|
|
184
|
+
changelog_uri: https://github.com/Agrimatics/activemodel-datastore/blob/main/CHANGELOG.md
|
|
186
185
|
source_code_uri: https://github.com/Agrimatics/activemodel-datastore/
|
|
187
186
|
bug_tracker_uri: https://github.com/Agrimatics/activemodel-datastore/issues
|
|
188
|
-
post_install_message:
|
|
189
187
|
rdoc_options: []
|
|
190
188
|
require_paths:
|
|
191
189
|
- lib
|
|
@@ -200,8 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
200
198
|
- !ruby/object:Gem::Version
|
|
201
199
|
version: '0'
|
|
202
200
|
requirements: []
|
|
203
|
-
rubygems_version:
|
|
204
|
-
signing_key:
|
|
201
|
+
rubygems_version: 4.0.19
|
|
205
202
|
specification_version: 4
|
|
206
203
|
summary: Cloud Datastore integration with Active Model
|
|
207
204
|
test_files: []
|