created_id 1.1.0 → 1.1.2
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 +18 -0
- data/README.md +6 -2
- data/VERSION +1 -1
- data/lib/created_id/id_range.rb +24 -13
- data/lib/created_id.rb +24 -4
- metadata +3 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41989b3e85e6a81b89589bbc05a24f9e7bfcba4c47a974964ec5dfa96b6268f0
|
|
4
|
+
data.tar.gz: 1fb19ba275fbfff80f75bbc9be0242597f1997989922420eb525e1a93b84cf2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ef459aad9deec426b20ae18867380b880126730351d677f8b7e15d8f07900566d5fc7fc779845e9b047131584dbc2b59f1d8854bdb8f52d7f38fe5697fdf11e
|
|
7
|
+
data.tar.gz: b8016654eeb70c0b750ebefcc497aedacad8fe49f994c716fcce278ab61041c6ff0f9f0b140ab437760cd85aad25c8927e48e24394a49529fd31bd6291346f9b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 1.1.2
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Fixed data corruption when re-running `index_ids_for` on an hour that had already been indexed. The range calculation was using the hour's own stored range as a bound, which excluded the hour's true minimum id (and could exclude the maximum) and caused records to be silently dropped from `created_between`, `created_after`, and `created_before` query results. Recalculating an indexed hour is now safe.
|
|
12
|
+
- The upper bound used when calculating an hour's id range now always skips the immediately following hour, since adjacent hours' id ranges can legitimately overlap when ids are slightly out of order near an hour boundary.
|
|
13
|
+
- Fixed a race condition in `index_ids_for` where two processes indexing the same class and hour concurrently could raise `ActiveRecord::RecordNotUnique`. The save is now retried so the losing process updates the existing row.
|
|
14
|
+
- Fixed misspelled `ArgumentError` constant that caused a `NameError` when including `CreatedId` in a non-ActiveRecord class.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Creating a record with an explicitly backdated `created_at` in an hour whose id range has already been indexed now raises `CreatedId::CreatedAtChangedError` and rolls back the insert, since the record's id would fall outside the stored range and be invisible to range queries.
|
|
19
|
+
|
|
20
|
+
## 1.1.1
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fixed issue with finding id ranges that prevented the query from using the indexed ids.
|
|
7
25
|
## 1.1.0
|
|
8
26
|
|
|
9
27
|
### Changed
|
data/README.md
CHANGED
|
@@ -111,12 +111,14 @@ Task.where(user_id: 1000).created_before(7.days.ago)
|
|
|
111
111
|
Task.created_between(25.hours.ago, 24.hours.ago)
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
You'll then need to set up a periodic task to store the id ranges for your models. For each model that includes `CreatedId`, you need to run the `index_ids_for` once per hour. This task should be run shortly after the top of the hour.
|
|
114
|
+
You'll then need to set up a periodic task to store the id ranges for your models. For each model that includes `CreatedId`, you need to run the `index_ids_for` once per hour. This task should be run shortly after the top of the hour, but leave a few minutes of buffer — longer than your longest-running write transaction. A transaction that starts at the end of an hour but commits after the indexer has scanned that hour would otherwise not be visible to the indexer, and if that row happens to be the hour's minimum or maximum id, it would fall outside the stored range and be missed by queries.
|
|
115
115
|
|
|
116
116
|
```ruby
|
|
117
117
|
Task.index_ids_for(1.hour.ago)
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
Re-running `index_ids_for` for an hour that has already been indexed is safe and recalculates the stored range from the current data. As a self-healing measure, you can periodically re-index the last few hours to pick up any rows that were committed after their hour was first indexed.
|
|
121
|
+
|
|
120
122
|
Finally, you'll need to run a script to calculate the id ranges for all of your existing data.
|
|
121
123
|
|
|
122
124
|
```ruby
|
|
@@ -130,7 +132,9 @@ end
|
|
|
130
132
|
|
|
131
133
|
If an ID range is missing for a specific hour, your queries will still function, but with a broader range of IDs. You can recalculate missing ranges at any time to improve efficiency.
|
|
132
134
|
|
|
133
|
-
There is an additional requirement for using this gem that you do not change the `created_at` value after a row is inserted since this can mess up the assumption about the correlation between ids and `created_at` timestamps. An error will be thrown if you try to change a record's timestamp after the id range has been created. The query logic can handle small variations between id order and timestamp order (i.e. if id 1000 has a timestamp a few seconds after id 1001).
|
|
135
|
+
There is an additional requirement for using this gem that you do not change the `created_at` value after a row is inserted since this can mess up the assumption about the correlation between ids and `created_at` timestamps. An error will be thrown if you try to change a record's timestamp after the id range has been created, or if you insert a new record with a backdated `created_at` in an hour whose id range has already been indexed. The query logic can handle small variations between id order and timestamp order (i.e. if id 1000 has a timestamp a few seconds after id 1001).
|
|
136
|
+
|
|
137
|
+
Note that these guards can only check hours that have already been indexed. Changing `created_at` across hours (or inserting backdated rows) into an hour that has *not* been indexed can still leave the row outside the id bounds inferred from neighboring indexed hours. If you need to backfill or correct historical data, re-run `index_ids_for` for the affected hours afterwards.
|
|
134
138
|
|
|
135
139
|
## Installation
|
|
136
140
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.1.
|
|
1
|
+
1.1.2
|
data/lib/created_id/id_range.rb
CHANGED
|
@@ -68,18 +68,21 @@ module CreatedId
|
|
|
68
68
|
klass = klass.base_class
|
|
69
69
|
hour = CreatedId.coerce_hour(time)
|
|
70
70
|
next_hour = hour + 3600
|
|
71
|
-
prev_hour = hour - 3600
|
|
72
71
|
|
|
73
72
|
finder = klass.unscoped.where(created_at: (hour...next_hour))
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
# Lower bound: the min id of the latest indexed hour strictly before this one.
|
|
75
|
+
# A record in this hour can never have an id at or below the previous indexed
|
|
76
|
+
# hour's minimum, even when ids are out of order near the boundary.
|
|
77
|
+
prev_id = for_class(klass).where(hour: nil...hour).order(hour: :desc).first&.min_id
|
|
78
|
+
finder = finder.where(klass.arel_table[:id].gt(prev_id)) if prev_id && prev_id > 0
|
|
79
|
+
|
|
80
|
+
# Upper bound: the min id of the earliest indexed hour at least two hours after
|
|
81
|
+
# this one. The immediately following hour is skipped because its id range may
|
|
82
|
+
# legitimately overlap this hour's when ids are out of order near the boundary.
|
|
83
|
+
next_id = for_class(klass).created_after(next_hour + 3600).order(hour: :asc).first&.min_id
|
|
84
|
+
if next_id && (prev_id.nil? || next_id > prev_id)
|
|
85
|
+
finder = finder.where(klass.arel_table[:id].lt(next_id))
|
|
83
86
|
end
|
|
84
87
|
|
|
85
88
|
[finder.minimum(:id), finder.maximum(:id)]
|
|
@@ -93,10 +96,18 @@ module CreatedId
|
|
|
93
96
|
# @param max_id [Integer] The maximum id for the class created in the given hour.
|
|
94
97
|
# @return [void]
|
|
95
98
|
def save_created_id(klass, time, min_id, max_id)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
retried = false
|
|
100
|
+
begin
|
|
101
|
+
record = find_or_initialize_by(class_name: klass.base_class.name, hour: CreatedId.coerce_hour(time))
|
|
102
|
+
record.min_id = min_id
|
|
103
|
+
record.max_id = max_id
|
|
104
|
+
record.save!
|
|
105
|
+
rescue ActiveRecord::RecordNotUnique
|
|
106
|
+
# A concurrent process indexed the same hour first; retry to update its record.
|
|
107
|
+
raise if retried
|
|
108
|
+
retried = true
|
|
109
|
+
retry
|
|
110
|
+
end
|
|
100
111
|
end
|
|
101
112
|
end
|
|
102
113
|
|
data/lib/created_id.rb
CHANGED
|
@@ -21,13 +21,14 @@ module CreatedId
|
|
|
21
21
|
|
|
22
22
|
included do
|
|
23
23
|
unless defined?(ActiveRecord) && self < ActiveRecord::Base
|
|
24
|
-
raise
|
|
24
|
+
raise ArgumentError, "CreatedId can only be included in ActiveRecord models"
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
scope :created_after, ->(time) { created_between(time, nil) }
|
|
28
28
|
scope :created_before, ->(time) { created_between(nil, time) }
|
|
29
29
|
|
|
30
30
|
before_save :verify_created_at_created_id!, if: :created_at_changed?
|
|
31
|
+
after_create :verify_created_id_on_backdated_create!
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
class_methods do
|
|
@@ -71,11 +72,30 @@ module CreatedId
|
|
|
71
72
|
# This is the normal case where created at is set to the current time on insert.
|
|
72
73
|
return if id.nil? && created_at_was.nil?
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
verify_id_in_indexed_range!("created_at cannot be changed outside of the range of the created_ids for that time period")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Verify that a record created with an explicit created_at in an hour that has already
|
|
79
|
+
# been indexed received an id within the stored range. The id is not known until after
|
|
80
|
+
# the insert, so this check must run after create; raising here rolls back the insert.
|
|
81
|
+
# Records created in the current hour skip the check since that hour cannot have been
|
|
82
|
+
# indexed yet.
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
# @raise [CreatedId::CreatedAtChangedError] If the record's id is outside the range of the created_ids for its hour.
|
|
86
|
+
def verify_created_id_on_backdated_create!
|
|
87
|
+
return if created_at.nil?
|
|
88
|
+
return if CreatedId.coerce_hour(created_at) == CreatedId.coerce_hour(Time.now)
|
|
89
|
+
|
|
90
|
+
verify_id_in_indexed_range!("created_at cannot be set to an hour whose id range has already been indexed")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def verify_id_in_indexed_range!(message)
|
|
94
|
+
hour = CreatedId.coerce_hour(created_at || Time.now)
|
|
95
|
+
range = CreatedId::IdRange.for_class(self.class).find_by(hour: hour)
|
|
76
96
|
|
|
77
97
|
if range && (id < range.min_id || id > range.max_id)
|
|
78
|
-
raise CreatedAtChangedError,
|
|
98
|
+
raise CreatedAtChangedError, message
|
|
79
99
|
end
|
|
80
100
|
end
|
|
81
101
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: created_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
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: activerecord
|
|
@@ -38,7 +37,6 @@ dependencies:
|
|
|
38
37
|
- - ">="
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '0'
|
|
41
|
-
description:
|
|
42
40
|
email:
|
|
43
41
|
- bbdurand@gmail.com
|
|
44
42
|
executables: []
|
|
@@ -62,7 +60,6 @@ metadata:
|
|
|
62
60
|
homepage_uri: https://github.com/bdurand/created_id
|
|
63
61
|
source_code_uri: https://github.com/bdurand/created_id
|
|
64
62
|
changelog_uri: https://github.com/bdurand/created_id/blob/main/CHANGELOG.md
|
|
65
|
-
post_install_message:
|
|
66
63
|
rdoc_options: []
|
|
67
64
|
require_paths:
|
|
68
65
|
- lib
|
|
@@ -77,8 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
77
74
|
- !ruby/object:Gem::Version
|
|
78
75
|
version: '0'
|
|
79
76
|
requirements: []
|
|
80
|
-
rubygems_version:
|
|
81
|
-
signing_key:
|
|
77
|
+
rubygems_version: 4.0.3
|
|
82
78
|
specification_version: 4
|
|
83
79
|
summary: Optimize ActiveRecord queries for filtering large tables on the created_at
|
|
84
80
|
column by pre-computing id ranges.
|