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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c26efa635bdc60b33239d0f6b33cc6e4ac86fb5c752672f25f6969b2c3dda5c
4
- data.tar.gz: 9651de6765b071963bbf0ee9da4680dbb46c5d0b820d463e34db0fc0e3d36612
3
+ metadata.gz: 62ccc081e2437c2c1ca324b12a78f888cb14bf5fb0d6cabe8ebee0c1089f3807
4
+ data.tar.gz: 5def43f01da73999c34f0f2a6bc66ead4249eda5df08281dd552da42856b0fea
5
5
  SHA512:
6
- metadata.gz: 4227107ad98274df5bac98ffb38e7de8ebfc8ec3d61edf4f09497ad1498fe936cf2a4a10a4c583ba43f0288f0f011cd8b20dd454f8d2f677380c6967dc3c4c9c
7
- data.tar.gz: 38d33728917c185c9beaf6c5ecb64e6adcf5e94ab672651ea33a41d22e9a9b6be503ee043b8878fc19aa81b552b7eae662cc568a50bf073853782bd45e568770
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://github.com/GoogleCloudPlatform/google-cloud-ruby/tree/master/google-cloud-datastore) gem compliant with [active_model](https://github.com/rails/rails/tree/master/activemodel) conventions and compatible with your Rails 5+ applications.
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 [Cloud Datastore](https://cloud.google.com/datastore)
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
- When you want a Rails app backed by a managed, massively-scalable datastore solution. Cloud Datastore
11
- automatically handles sharding and replication. It is a highly available and durable database that
12
- automatically scales to handle your applications' load. Cloud Datastore is a schemaless database
13
- suited for unstructured or semi-structured application data.
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
  [![Gem Version](https://badge.fury.io/rb/activemodel-datastore.svg)](https://badge.fury.io/rb/activemodel-datastore)
16
- [![Build Status](https://travis-ci.org/Agrimatics/activemodel-datastore.svg?branch=master)](https://travis-ci.org/Agrimatics/activemodel-datastore)
18
+ [![CI](https://github.com/Agrimatics/activemodel-datastore/actions/workflows/ci.yml/badge.svg?branch=main)](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
- - [Datastore Emulator](#emulator)
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
- Google Cloud requires the Project ID and Service Account Credentials to connect to the Datastore API.
52
-
53
- *Follow the [activation instructions](https://cloud.google.com/datastore/docs/activate) to enable the
54
- Google Cloud Datastore API. When running on Google Cloud Platform environments the Service Account
55
- credentials will be discovered automatically. When running on other environments (such as AWS or Heroku)
56
- you need to create a service account with the role of editor and generate json credentials.*
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
- If you have an external application running on a platform outside of Google Cloud you also need to
67
- provide the Service Account credentials. They are specified in two additional `ENV` variables named
68
- `SERVICE_ACCOUNT_CLIENT_EMAIL` and `SERVICE_ACCOUNT_PRIVATE_KEY`. The values for these two `ENV`
69
- variables will be in the downloaded service account json credentials file.
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 = -----BEGIN PRIVATE KEY-----\nMIIFfb3...5dmFtABy\n-----END PRIVATE KEY-----\n
73
- SERVICE_ACCOUNT_CLIENT_EMAIL = web-app@app-name.iam.gserviceaccount.com
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 `ENV` variables can be set under 'Settings' -> 'Config Variables'.
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
- There is an example Puma config file [here](https://github.com/Agrimatics/activemodel-datastore/blob/master/test/support/datastore_example_rails_app/config/puma.rb).
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 Cloud Datastore are known as entities. Entities are of a kind. An entity has one
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 cloud
106
- datastore. Define the attributes of your model using `attr_accessor`. With this approach, Rails
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 datastore.
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 Cloud Datastore has a key that uniquely identifies it. The key consists of the
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 Cloud Datastore hierarchy
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
- Cloud Datastore has documentation on how [Datastore Queries](https://cloud.google.com/datastore/docs/concepts/queries#datastore-basic-query-ruby)
283
- work, and pay special attention to the the [restrictions](https://cloud.google.com/datastore/docs/concepts/queries#restrictions_on_queries).
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
- Cloud Datastore is a non-relational databases, or NoSQL database. It distributes data over many
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
- What is eventual consistency?
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
- It means that an updated entity value may not be immediately visible when executing a query.
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
- In the context of a Rails app, there are times that eventual consistency is not ideal. For example,
298
- let's say you create a user entity with a key that looks like this:
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
- `@key=#<Google::Cloud::Datastore::Key @kind="User", @id=1>`
320
+ Use the following command to inspect a database's concurrency mode:
301
321
 
302
- and then immediately redirect to the index view of users. There is a good chance that your new user
303
- is not yet visible in the list. If you perform a refresh on the index view a second or two later
304
- the user will appear.
322
+ ```bash
323
+ gcloud firestore databases describe --project=PROJECT_ID --database=DATABASE_ID
324
+ ```
305
325
 
306
- "Wait a minute!" you say. "This is crap!" you say. Fear not! We can make the query of users strongly
307
- consistent. We just need to use entity groups and ancestor queries. An entity group is a hierarchy
308
- formed by a root entity and its children. To create an entity group, you specify an ancestor path
309
- for the entity which is a parent key as part of the child key.
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
- *Ancestor queries are always strongly consistent.*
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 here for the Cloud Datastore documentation on [Data Consistency](https://cloud.google.com/datastore/docs/concepts/structuring_for_strong_consistency).
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 cloud datastore query requires an index. Yes, you read that correctly. Every single one. The
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, Cloud Datastore automatically predefines an index for each property of each entity kind.
399
- These single property indexes are suitable for simple types of queries. These indexes are free and
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 index multiple property values per indexed entity. Composite indexes support complex
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 has excellent doc regarding datastore indexes [here](https://cloud.google.com/datastore/docs/concepts/indexes).
417
-
418
- The datastore emulator generates composite indexes in an index.yaml file automatically. The file
419
- can be found in /tmp/local_datastore/WEB-INF/index.yaml. If your localhost Rails app exercises every
420
- possible query the application will issue, using every combination of filter and sort order, the
421
- generated entries will represent your complete set of indexes.
422
-
423
- One thing to note is that the datastore emulator caches indexes. As you add and modify application
424
- code you might find that the local datastore index.yaml contains indexes that are no longer needed.
425
- In this scenario try deleting the index.yaml and restarting the emulator. Navigate through your Rails
426
- app and the index.yaml will be built from scratch.
427
-
428
- ## <a name="emulator"></a>Datastore Emulator
429
-
430
- Install the Google Cloud SDK.
431
-
432
- $ curl https://sdk.cloud.google.com | bash
433
-
434
- You can check the version of the SDK and the components installed with:
435
-
436
- $ gcloud components list
437
-
438
- Install the Cloud Datastore Emulator, which provides local emulation of the production Cloud
439
- Datastore environment and the gRPC API. However, you'll need to do a small amount of configuration
440
- before running the application against the emulator, see [here.](https://cloud.google.com/datastore/docs/tools/datastore-emulator)
441
-
442
- $ gcloud components install cloud-datastore-emulator
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 5 app in the test directory [here](https://github.com/Agrimatics/activemodel-datastore/tree/master/test/support/datastore_example_rails_app).
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
- ```bash
469
- $ bundle
470
- $ cloud_datastore_emulator create tmp/local_datastore
471
- $ cloud_datastore_emulator create tmp/test_datastore
472
- $ ./start-local-datastore.sh
473
- $ rails s
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
- Alternatively,`:reject_if` also accepts a symbol for using methods:
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
- reject_proc = proc { |attributes| attributes['name'].blank? }
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 nested_models will have `marked_for_destruction` set
598
- to True.
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 Cloud Datastore implementation evolves (or if a project's indexes change), this order may change.
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? { CloudDatastore.dataset.save entity }
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.merge(value: value))
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? ? true : false
74
+ changed = marked_for_destruction? || false
75
75
  tracked_attributes.each do |attr|
76
76
  break if changed
77
77
 
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  module Datastore
3
- VERSION = '0.8.0'
3
+ VERSION = '0.10.0'
4
4
  end
5
5
  end
@@ -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? { CloudDatastore.dataset.save entity }
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
- retry_on_exception { CloudDatastore.dataset.find key }
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 = retry_on_exception { CloudDatastore.dataset.run query }
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 = retry_on_exception { CloudDatastore.dataset.run query }
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
- return false if retries >= max_retry_count
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
- raise e if retries >= max_retry_count
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
- retry_on_exception { CloudDatastore.dataset.find_all keys }
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.8.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: 2024-03-05 00:00:00.000000000 Z
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: '2.1'
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: '2.1'
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/master/CHANGELOG.md
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: 3.5.3
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: []