Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ gem 'strong_migrations' # Use safe Migration patterns

gem 'rubocop'

gem "activerecord-slotted_counters"

group :development, :test do
gem 'active_record_doctor'
gem 'benchmark-ips'
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ GEM
timeout (>= 0.4.0)
activerecord-import (1.8.1)
activerecord (>= 4.2)
activerecord-slotted_counters (0.3.0)
activerecord (>= 6.1)
activestorage (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
Expand Down Expand Up @@ -358,6 +360,7 @@ PLATFORMS
DEPENDENCIES
active_record_doctor
activerecord-import (~> 1.5)
activerecord-slotted_counters
bcrypt (~> 3.1)
benchmark-ips
benchmark-memory
Expand Down
3 changes: 3 additions & 0 deletions app/models/rider.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class Rider < User
# https://github.com/evilmartians/activerecord-slotted_counters
has_slotted_counter :trip_requests

has_many :trip_requests
has_many :trips, through: :trip_requests
end
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class User < ApplicationRecord
# counter_cache trips_count
has_secure_password
validates :first_name, :last_name, presence: true
validates :drivers_license_number,
Expand Down
15 changes: 15 additions & 0 deletions db/migrate/20250617150055_create_slotted_counters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class CreateSlottedCounters < ActiveRecord::Migration[7.2]
def change
create_table :slotted_counters do |t|
t.string :counter_name, null: false
t.string :associated_record_type, null: false
t.integer :associated_record_id, null: false
t.integer :slot, null: false
t.integer :count, null: false

t.timestamps
end

add_index :slotted_counters, [:associated_record_id, :associated_record_type, :counter_name, :slot], unique: true, name: 'index_slotted_counters'
end
end
63 changes: 63 additions & 0 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ DROP INDEX IF EXISTS rideshare.index_trips_on_driver_id;
DROP INDEX IF EXISTS rideshare.index_trip_requests_on_start_location_id;
DROP INDEX IF EXISTS rideshare.index_trip_requests_on_rider_id;
DROP INDEX IF EXISTS rideshare.index_trip_requests_on_end_location_id;
DROP INDEX IF EXISTS rideshare.index_slotted_counters;
DROP INDEX IF EXISTS rideshare.index_locations_on_address;
DROP INDEX IF EXISTS rideshare.index_fast_search_results_on_driver_id;
ALTER TABLE IF EXISTS ONLY rideshare.vehicles DROP CONSTRAINT IF EXISTS vehicles_pkey;
Expand All @@ -35,6 +36,7 @@ ALTER TABLE IF EXISTS ONLY rideshare.users DROP CONSTRAINT IF EXISTS users_pkey;
ALTER TABLE IF EXISTS ONLY rideshare.trips DROP CONSTRAINT IF EXISTS trips_pkey;
ALTER TABLE IF EXISTS ONLY rideshare.trip_requests DROP CONSTRAINT IF EXISTS trip_requests_pkey;
ALTER TABLE IF EXISTS ONLY rideshare.trip_positions DROP CONSTRAINT IF EXISTS trip_positions_pkey;
ALTER TABLE IF EXISTS ONLY rideshare.slotted_counters DROP CONSTRAINT IF EXISTS slotted_counters_pkey;
ALTER TABLE IF EXISTS ONLY rideshare.schema_migrations DROP CONSTRAINT IF EXISTS schema_migrations_pkey;
ALTER TABLE IF EXISTS ONLY rideshare.vehicle_reservations DROP CONSTRAINT IF EXISTS non_overlapping_vehicle_registration;
ALTER TABLE IF EXISTS ONLY rideshare.locations DROP CONSTRAINT IF EXISTS locations_pkey;
Expand All @@ -46,6 +48,7 @@ ALTER TABLE IF EXISTS rideshare.users ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS rideshare.trips ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS rideshare.trip_requests ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS rideshare.trip_positions ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS rideshare.slotted_counters ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS rideshare.locations ALTER COLUMN id DROP DEFAULT;
DROP SEQUENCE IF EXISTS rideshare.vehicles_id_seq;
DROP TABLE IF EXISTS rideshare.vehicles;
Expand All @@ -57,6 +60,8 @@ DROP SEQUENCE IF EXISTS rideshare.trip_requests_id_seq;
DROP TABLE IF EXISTS rideshare.trip_requests;
DROP SEQUENCE IF EXISTS rideshare.trip_positions_id_seq;
DROP TABLE IF EXISTS rideshare.trip_positions;
DROP SEQUENCE IF EXISTS rideshare.slotted_counters_id_seq;
DROP TABLE IF EXISTS rideshare.slotted_counters;
DROP VIEW IF EXISTS rideshare.search_results;
DROP TABLE IF EXISTS rideshare.schema_migrations;
DROP SEQUENCE IF EXISTS rideshare.locations_id_seq;
Expand Down Expand Up @@ -314,6 +319,41 @@ CREATE VIEW rideshare.search_results AS
ORDER BY (count(t.rating)) DESC;


--
-- Name: slotted_counters; Type: TABLE; Schema: rideshare; Owner: -
--

CREATE TABLE rideshare.slotted_counters (
id bigint NOT NULL,
counter_name character varying NOT NULL,
associated_record_type character varying NOT NULL,
associated_record_id integer NOT NULL,
slot integer NOT NULL,
count integer NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);


--
-- Name: slotted_counters_id_seq; Type: SEQUENCE; Schema: rideshare; Owner: -
--

CREATE SEQUENCE rideshare.slotted_counters_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;


--
-- Name: slotted_counters_id_seq; Type: SEQUENCE OWNED BY; Schema: rideshare; Owner: -
--

ALTER SEQUENCE rideshare.slotted_counters_id_seq OWNED BY rideshare.slotted_counters.id;


--
-- Name: trip_positions; Type: TABLE; Schema: rideshare; Owner: -
--
Expand Down Expand Up @@ -491,6 +531,13 @@ ALTER SEQUENCE rideshare.vehicles_id_seq OWNED BY rideshare.vehicles.id;
ALTER TABLE ONLY rideshare.locations ALTER COLUMN id SET DEFAULT nextval('rideshare.locations_id_seq'::regclass);


--
-- Name: slotted_counters id; Type: DEFAULT; Schema: rideshare; Owner: -
--

ALTER TABLE ONLY rideshare.slotted_counters ALTER COLUMN id SET DEFAULT nextval('rideshare.slotted_counters_id_seq'::regclass);


--
-- Name: trip_positions id; Type: DEFAULT; Schema: rideshare; Owner: -
--
Expand Down Expand Up @@ -573,6 +620,14 @@ ALTER TABLE ONLY rideshare.schema_migrations
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);


--
-- Name: slotted_counters slotted_counters_pkey; Type: CONSTRAINT; Schema: rideshare; Owner: -
--

ALTER TABLE ONLY rideshare.slotted_counters
ADD CONSTRAINT slotted_counters_pkey PRIMARY KEY (id);


--
-- Name: trip_positions trip_positions_pkey; Type: CONSTRAINT; Schema: rideshare; Owner: -
--
Expand Down Expand Up @@ -635,6 +690,13 @@ CREATE UNIQUE INDEX index_fast_search_results_on_driver_id ON rideshare.fast_sea
CREATE UNIQUE INDEX index_locations_on_address ON rideshare.locations USING btree (address);


--
-- Name: index_slotted_counters; Type: INDEX; Schema: rideshare; Owner: -
--

CREATE UNIQUE INDEX index_slotted_counters ON rideshare.slotted_counters USING btree (associated_record_id, associated_record_type, counter_name, slot);


--
-- Name: index_trip_requests_on_end_location_id; Type: INDEX; Schema: rideshare; Owner: -
--
Expand Down Expand Up @@ -776,6 +838,7 @@ ALTER TABLE ONLY rideshare.trip_requests
SET search_path TO rideshare;

INSERT INTO "schema_migrations" (version) VALUES
('20250617150055'),
('20231220043547'),
('20231218215836'),
('20231213045957'),
Expand Down
26 changes: 26 additions & 0 deletions docs/slotted_counters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Slotted Counters

A Rider can track its trip_requests.

```rb
# https://github.com/evilmartians/activerecord-slotted_counters
has_slotted_counter :trip_requests
```

Increment counter manually
```
rideshare(dev)> Rider.increment_counter(:trip_requests_count, rider.id)
TRANSACTION (9.1ms) BEGIN
(2.3ms) INSERT INTO "slotted_counters" ("counter_name","associated_record_type","associated_record_id","slot","count","created_at","updated_at") VALUES ('trip_requests_count', 'Rider', 20200, 86, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) ON CONFLICT ("associated_record_id","associated_record_type","counter_name","slot") DO UPDATE SET count = slotted_counters.count + EXCLUDED.count RETURNING "id"
TRANSACTION (0.3ms) COMMIT
```

Access:
```
rider.trip_requests_count
```

## Why the "slot"?
By having a limit of rand(100), there are up to 100 possible records representing the total count.

A slot keeps a counter, and its counter is updated on increment. This distributes the updates and avoids locking the same row.
15 changes: 15 additions & 0 deletions docs/slotted_counters_bench.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Run with Rails Runner
rider = Rider.last
10_000.times do
Rider.increment_counter(:trip_requests_count, rider.id)
end

# Although there are 10,000 increments for the same
# Rider, we don't have 10K records in slotted_counters
# due to the "slot" distributing the inserts
#
# This controls the growth of slotter_counters
# compared with a insert-per-increment approach

puts "getting Rider trip_requests_count"
puts rider.trip_requests_count
Binary file modified erd.pdf
Binary file not shown.