Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debounced updatable #261

Merged
merged 5 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
46 changes: 44 additions & 2 deletions app/models/concerns/cable_ready/updatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ module CableReady
module Updatable
extend ::ActiveSupport::Concern

class MemoryDebounceAdapter
include Singleton
include MonitorMixin

delegate_missing_to :@dict

def initialize
super
@dict = {}
end

def []=(key, value)
synchronize do
@dict[key] = value
end
end
julianrubisch marked this conversation as resolved.
Show resolved Hide resolved
end

mattr_accessor :debounce_adapter, default: MemoryDebounceAdapter.instance

included do |base|
if defined?(ActiveRecord) && base < ActiveRecord::Base
include ExtendHasMany
Expand All @@ -30,11 +50,14 @@ def self.enable_cable_ready_updates(*options)
options = options.extract_options!
options = {
on: [:create, :update, :destroy],
if: -> { true }
if: -> { true },
debounce: 0.seconds
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would make sense to have the default provide some degree of protection. Perhaps 100 milliseconds?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well I switch on the debounce time in broadcast_updates to gracefully allow the default behavior. But sure, why not!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually I just noticed that this breaks all the tests, so we'd have to specify debounce: 0 everywhere. I'd recommend merging this PR and then introducing this in a separate one to keep things readable

}.merge(options)

enabled_operations = Array(options[:on])

@debounce_time = options[:debounce]

after_commit(ModelUpdatableCallbacks.new(:create, enabled_operations), {on: :create, if: options[:if]})
after_commit(ModelUpdatableCallbacks.new(:update, enabled_operations), {on: :update, if: options[:if]})
after_commit(ModelUpdatableCallbacks.new(:destroy, enabled_operations), {on: :destroy, if: options[:if]})
Expand All @@ -52,6 +75,8 @@ def self.skip_cable_ready_updates
private

module ClassMethods
include Compoundable

def has_many(name, scope = nil, **options, &extension)
option = if options.has_key?(:enable_updates)
warn "DEPRECATED: please use `enable_cable_ready_updates` instead. The `enable_updates` option will be removed from a future version of CableReady 5"
Expand Down Expand Up @@ -180,7 +205,24 @@ def build_options(option)
def broadcast_updates(model_class, options)
return if skip_updates_classes.any? { |klass| klass >= self }
raise("ActionCable must be enabled to use Updatable") unless defined?(ActionCable)
ActionCable.server.broadcast(model_class, options)

if debounce_time > 0.seconds
key = compound([model_class, *options])
old_wait_until = CableReady::Updatable.debounce_adapter[key]
now = Time.now.to_f

if old_wait_until.nil? || old_wait_until < now
new_wait_until = now + debounce_time.to_f
CableReady::Updatable.debounce_adapter[key] = new_wait_until
ActionCable.server.broadcast(model_class, options)
end
else
ActionCable.server.broadcast(model_class, options)
end
end

def debounce_time
@debounce_time ||= 0.seconds
end

def skip_updates_classes
Expand Down
5 changes: 5 additions & 0 deletions test/dummy/app/models/site.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Site < ApplicationRecord
include CableReady::Updatable

enable_cable_ready_updates debounce: 3.seconds
end
9 changes: 9 additions & 0 deletions test/dummy/db/migrate/20230301080311_create_sites.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class CreateSites < ActiveRecord::Migration[6.1]
def change
create_table :sites do |t|
t.string :name

t.timestamps
end
end
end
10 changes: 7 additions & 3 deletions test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# frozen_string_literal: true

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
Expand All @@ -12,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2022_05_18_072701) do
ActiveRecord::Schema.define(version: 2023_03_01_080311) do

create_table "accounts", force: :cascade do |t|
t.string "account_number"
Expand Down Expand Up @@ -78,6 +76,12 @@
t.datetime "updated_at", precision: 6, null: false
end

create_table "sites", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end

create_table "suppliers", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
Expand Down
7 changes: 7 additions & 0 deletions test/dummy/test/fixtures/sites.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

one:
name: MyString

two:
name: MyString
7 changes: 7 additions & 0 deletions test/dummy/test/models/site_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "test_helper"

class SiteTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
17 changes: 17 additions & 0 deletions test/lib/cable_ready/updatable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,21 @@ class TestEnableUpdatesOption < ActiveRecord::Base
end
end
# standard:enable Lint/ConstantDefinitionInBlock

test "only sends out a ping once per debounce period" do
site = Site.create(name: "Front Page")

mock_server = mock("server")
mock_server.expects(:broadcast).with(Site, {changed: ["name", "updated_at"]}).twice
mock_server.expects(:broadcast).with(site.to_global_id, {changed: ["name", "updated_at"]}).twice

ActionCable.stubs(:server).returns(mock_server)

# debounce time is 3 seconds, so the last update should trigger its own broadcast
site.update(name: "Landing Page 1")
travel(1.second)
site.update(name: "Landing Page 2")
travel(3.seconds)
site.update(name: "Landing Page 3")
end
end