{"action":"created","comment":{"url":"https://api.github.com/repos/rails/rails/pulls/comments/24302971","id":24302971,"diff_hunk":"@@ -0,0 +1,71 @@\n+module ActiveRecord\n+  # = Active Record Delay Touching\n+  module DelayTouching\n+\n+    # Tracking of the touch state. This class has no class-level data, so you can\n+    # store per-thread instances in thread-local variables.\n+    class State # :nodoc:\n+      def initialize\n+        @records = Hash.new { SortedSet.new }\n+        @already_updated_records = Hash.new { SortedSet.new }\n+      end\n+\n+      def updated(klass, attrs, records)\n+        @already_updated_records[[klass, attrs]] += records\n+      end\n+\n+      # Return the records grouped by class and columns that were touched:\n+      #\n+      #   {\n+      #     [Owner, [:updated_at]]               => SortedSet.new([owner1, owner2]),\n+      #     [Pet,   [:neutered_at, :updated_at]] => SortedSet.new([pet1]),\n+      #     [Pet,   [:updated_at]]               => SortedSet.new([pet2])\n+      #   }\n+      #\n+      # As a side-effect, clears out the list of records.\n+      def get_and_clear_records\n+        records = @records\n+        @records = Hash.new { SortedSet.new }\n+        records\n+      end\n+\n+      def more_records?\n+        @records.present?\n+      end\n+\n+      def add_record(record, columns)\n+        # Include the standard updated_at column and any additional specified columns\n+        updated_at_attrs = record.timestamp_attributes_for_update_in_model\n+        columns += updated_at_attrs if updated_at_attrs.present?\n+        columns = columns.map(&:to_sym).sort\n+\n+        @records[[record.class, columns]] += [record] unless @already_updated_records[[record.class, columns]].include?(record)\n+      end\n+\n+      def clear_records","path":"activerecord/lib/active_record/delay_touching/state.rb","position":45,"original_position":45,"commit_id":"3c1e227fc388d6874fb2981611c5ee9057704bd2","original_commit_id":"3c1e227fc388d6874fb2981611c5ee9057704bd2","user":{"login":"BMorearty","id":16927,"avatar_url":"https://avatars.githubusercontent.com/u/16927?v=3","gravatar_id":"","url":"https://api.github.com/users/BMorearty","html_url":"https://github.com/BMorearty","followers_url":"https://api.github.com/users/BMorearty/followers","following_url":"https://api.github.com/users/BMorearty/following{/other_user}","gists_url":"https://api.github.com/users/BMorearty/gists{/gist_id}","starred_url":"https://api.github.com/users/BMorearty/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BMorearty/subscriptions","organizations_url":"https://api.github.com/users/BMorearty/orgs","repos_url":"https://api.github.com/users/BMorearty/repos","events_url":"https://api.github.com/users/BMorearty/events{/privacy}","received_events_url":"https://api.github.com/users/BMorearty/received_events","type":"User","site_admin":false},"body":"Whoops. Good catch.Removed.","created_at":"2015-02-08T21:58:11Z","updated_at":"2015-02-08T21:58:11Z","html_url":"https://github.com/rails/rails/pull/18824#discussion_r24302971","pull_request_url":"https://api.github.com/repos/rails/rails/pulls/18824","_links":{"self":{"href":"https://api.github.com/repos/rails/rails/pulls/comments/24302971"},"html":{"href":"https://github.com/rails/rails/pull/18824#discussion_r24302971"},"pull_request":{"href":"https://api.github.com/repos/rails/rails/pulls/18824"}}},"pull_request":{"url":"https://api.github.com/repos/rails/rails/pulls/18824","id":28789855,"html_url":"https://github.com/rails/rails/pull/18824","diff_url":"https://github.com/rails/rails/pull/18824.diff","patch_url":"https://github.com/rails/rails/pull/18824.patch","issue_url":"https://api.github.com/repos/rails/rails/issues/18824","number":18824,"state":"open","locked":false,"title":"Batch up all `touch` calls made in a transaction.","user":{"login":"BMorearty","id":16927,"avatar_url":"https://avatars.githubusercontent.com/u/16927?v=3","gravatar_id":"","url":"https://api.github.com/users/BMorearty","html_url":"https://github.com/BMorearty","followers_url":"https://api.github.com/users/BMorearty/followers","following_url":"https://api.github.com/users/BMorearty/following{/other_user}","gists_url":"https://api.github.com/users/BMorearty/gists{/gist_id}","starred_url":"https://api.github.com/users/BMorearty/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BMorearty/subscriptions","organizations_url":"https://api.github.com/users/BMorearty/orgs","repos_url":"https://api.github.com/users/BMorearty/repos","events_url":"https://api.github.com/users/BMorearty/events{/privacy}","received_events_url":"https://api.github.com/users/BMorearty/received_events","type":"User","site_admin":false},"body":"Greatly reduces the number of database round-trips made when `touch` is called on a lot of records in a transaction. It does so by delaying the `touch` calls until just before the transaction is committed. At that point, it consolidates the touches into the smallest possible number of SQL UPDATEs by (a) only touching each record once, even if it was touched repeatedly in the block, and (2) touching multiple records of a single table simultaneously, using an IN clause.\r\n\r\n`after_touch` calls are also deferred until right after the database updates occur, just before the commit.\r\n\r\nThe original code is from the [activerecord-delay_touching](https://github.com/godaddy/activerecord-delay_touching/) gem, which has been in production on multiple sites for a bit over six months.\r\n\r\nSome benchmarks: \r\n\r\n1. I wrote a sample app where `Person has_many :pets` and `Pet belongs_to :person, touch: true`. So each time a pet is updated, the person who owns it is touched. I made a person who has 25 pets. Then I ran this code in Rails 5 with and without this change:\r\n\r\n    ```ruby\r\n    Benchmark.ips do |x| \r\n      ids = person.pet_ids\r\n      x.report do\r\n        h = ids.map { |id| { id:id, name:rand } }\r\n        # Update all 25 pets. Touches person 25x without the change, 1x with it.\r\n        person.update pets_attributes: h \r\n      end\r\n    end\r\n    ```\r\n  \r\n  The result was **52 iterations per second** without this change and **91 iterations per second** with it.\r\n  \r\n2. One of the production sites where activerecord-delay_touching has been in use is a Spree site. To benchmark this change, @mtuckergd created 6 products. He then added 75 variants to each product. He benchmarked code that processes changes to all of the products' variants and updates the master stock count for each of them. The Spree models that got saved were `Variant`, `StockItem`, and `StockMovement`. Three runs were with delay touching disabled, and three were with it enabled. Results:\r\n\r\n    Without delay touching (seconds) | With delay touching (seconds)\r\n    ------------------------------ | --------------------------\r\n    21.27 | 10.37\r\n    20.26 | 10.21\r\n    21.22 | 10.45\r\n    **Average:** | \r\n    20.92 | 10.34\r\n\r\n    That’s a 51% improvement to the average processing time.\r\n\r\nTests are included for edge cases such as rolling back a nested transaction, both with and without `:requires_new`.\r\n\r\nFixes rails/rails#18606.","created_at":"2015-02-06T07:26:59Z","updated_at":"2015-02-08T21:58:11Z","closed_at":null,"merged_at":null,"merge_commit_sha":"98c561342d23d77d7609281adc27ef7104585394","assignee":null,"milestone":{"url":"https://api.github.com/repos/rails/rails/milestones/34","labels_url":"https://api.github.com/repos/rails/rails/milestones/34/labels","id":665192,"number":34,"title":"5.0.0","description":null,"creator":{"login":"rafaelfranca","id":47848,"avatar_url":"https://avatars.githubusercontent.com/u/47848?v=3","gravatar_id":"","url":"https://api.github.com/users/rafaelfranca","html_url":"https://github.com/rafaelfranca","followers_url":"https://api.github.com/users/rafaelfranca/followers","following_url":"https://api.github.com/users/rafaelfranca/following{/other_user}","gists_url":"https://api.github.com/users/rafaelfranca/gists{/gist_id}","starred_url":"https://api.github.com/users/rafaelfranca/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rafaelfranca/subscriptions","organizations_url":"https://api.github.com/users/rafaelfranca/orgs","repos_url":"https://api.github.com/users/rafaelfranca/repos","events_url":"https://api.github.com/users/rafaelfranca/events{/privacy}","received_events_url":"https://api.github.com/users/rafaelfranca/received_events","type":"User","site_admin":false},"open_issues":99,"closed_issues":57,"state":"open","created_at":"2014-05-21T00:58:22Z","updated_at":"2015-02-08T01:11:43Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/rails/rails/pulls/18824/commits","review_comments_url":"https://api.github.com/repos/rails/rails/pulls/18824/comments","review_comment_url":"https://api.github.com/repos/rails/rails/pulls/comments/{number}","comments_url":"https://api.github.com/repos/rails/rails/issues/18824/comments","statuses_url":"https://api.github.com/repos/rails/rails/statuses/3c1e227fc388d6874fb2981611c5ee9057704bd2","head":{"label":"BMorearty:delay_touching","ref":"delay_touching","sha":"3c1e227fc388d6874fb2981611c5ee9057704bd2","user":{"login":"BMorearty","id":16927,"avatar_url":"https://avatars.githubusercontent.com/u/16927?v=3","gravatar_id":"","url":"https://api.github.com/users/BMorearty","html_url":"https://github.com/BMorearty","followers_url":"https://api.github.com/users/BMorearty/followers","following_url":"https://api.github.com/users/BMorearty/following{/other_user}","gists_url":"https://api.github.com/users/BMorearty/gists{/gist_id}","starred_url":"https://api.github.com/users/BMorearty/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BMorearty/subscriptions","organizations_url":"https://api.github.com/users/BMorearty/orgs","repos_url":"https://api.github.com/users/BMorearty/repos","events_url":"https://api.github.com/users/BMorearty/events{/privacy}","received_events_url":"https://api.github.com/users/BMorearty/received_events","type":"User","site_admin":false},"repo":{"id":1580963,"name":"rails","full_name":"BMorearty/rails","owner":{"login":"BMorearty","id":16927,"avatar_url":"https://avatars.githubusercontent.com/u/16927?v=3","gravatar_id":"","url":"https://api.github.com/users/BMorearty","html_url":"https://github.com/BMorearty","followers_url":"https://api.github.com/users/BMorearty/followers","following_url":"https://api.github.com/users/BMorearty/following{/other_user}","gists_url":"https://api.github.com/users/BMorearty/gists{/gist_id}","starred_url":"https://api.github.com/users/BMorearty/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BMorearty/subscriptions","organizations_url":"https://api.github.com/users/BMorearty/orgs","repos_url":"https://api.github.com/users/BMorearty/repos","events_url":"https://api.github.com/users/BMorearty/events{/privacy}","received_events_url":"https://api.github.com/users/BMorearty/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/BMorearty/rails","description":"The upstream repo is at rails/rails","fork":true,"url":"https://api.github.com/repos/BMorearty/rails","forks_url":"https://api.github.com/repos/BMorearty/rails/forks","keys_url":"https://api.github.com/repos/BMorearty/rails/keys{/key_id}","collaborators_url":"https://api.github.com/repos/BMorearty/rails/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/BMorearty/rails/teams","hooks_url":"https://api.github.com/repos/BMorearty/rails/hooks","issue_events_url":"https://api.github.com/repos/BMorearty/rails/issues/events{/number}","events_url":"https://api.github.com/repos/BMorearty/rails/events","assignees_url":"https://api.github.com/repos/BMorearty/rails/assignees{/user}","branches_url":"https://api.github.com/repos/BMorearty/rails/branches{/branch}","tags_url":"https://api.github.com/repos/BMorearty/rails/tags","blobs_url":"https://api.github.com/repos/BMorearty/rails/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/BMorearty/rails/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/BMorearty/rails/git/refs{/sha}","trees_url":"https://api.github.com/repos/BMorearty/rails/git/trees{/sha}","statuses_url":"https://api.github.com/repos/BMorearty/rails/statuses/{sha}","languages_url":"https://api.github.com/repos/BMorearty/rails/languages","stargazers_url":"https://api.github.com/repos/BMorearty/rails/stargazers","contributors_url":"https://api.github.com/repos/BMorearty/rails/contributors","subscribers_url":"https://api.github.com/repos/BMorearty/rails/subscribers","subscription_url":"https://api.github.com/repos/BMorearty/rails/subscription","commits_url":"https://api.github.com/repos/BMorearty/rails/commits{/sha}","git_commits_url":"https://api.github.com/repos/BMorearty/rails/git/commits{/sha}","comments_url":"https://api.github.com/repos/BMorearty/rails/comments{/number}","issue_comment_url":"https://api.github.com/repos/BMorearty/rails/issues/comments/{number}","contents_url":"https://api.github.com/repos/BMorearty/rails/contents/{+path}","compare_url":"https://api.github.com/repos/BMorearty/rails/compare/{base}...{head}","merges_url":"https://api.github.com/repos/BMorearty/rails/merges","archive_url":"https://api.github.com/repos/BMorearty/rails/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/BMorearty/rails/downloads","issues_url":"https://api.github.com/repos/BMorearty/rails/issues{/number}","pulls_url":"https://api.github.com/repos/BMorearty/rails/pulls{/number}","milestones_url":"https://api.github.com/repos/BMorearty/rails/milestones{/number}","notifications_url":"https://api.github.com/repos/BMorearty/rails/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/BMorearty/rails/labels{/name}","releases_url":"https://api.github.com/repos/BMorearty/rails/releases{/id}","created_at":"2011-04-07T06:32:10Z","updated_at":"2015-02-06T04:56:05Z","pushed_at":"2015-02-06T20:31:20Z","git_url":"git://github.com/BMorearty/rails.git","ssh_url":"git@github.com:BMorearty/rails.git","clone_url":"https://github.com/BMorearty/rails.git","svn_url":"https://github.com/BMorearty/rails","homepage":"http://rubyonrails.org","size":114801,"stargazers_count":1,"watchers_count":1,"language":"Ruby","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":1,"default_branch":"master"}},"base":{"label":"rails:master","ref":"master","sha":"25bbe595f1dda1378238a4698b5f980a24110c75","user":{"login":"rails","id":4223,"avatar_url":"https://avatars.githubusercontent.com/u/4223?v=3","gravatar_id":"","url":"https://api.github.com/users/rails","html_url":"https://github.com/rails","followers_url":"https://api.github.com/users/rails/followers","following_url":"https://api.github.com/users/rails/following{/other_user}","gists_url":"https://api.github.com/users/rails/gists{/gist_id}","starred_url":"https://api.github.com/users/rails/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rails/subscriptions","organizations_url":"https://api.github.com/users/rails/orgs","repos_url":"https://api.github.com/users/rails/repos","events_url":"https://api.github.com/users/rails/events{/privacy}","received_events_url":"https://api.github.com/users/rails/received_events","type":"Organization","site_admin":false},"repo":{"id":8514,"name":"rails","full_name":"rails/rails","owner":{"login":"rails","id":4223,"avatar_url":"https://avatars.githubusercontent.com/u/4223?v=3","gravatar_id":"","url":"https://api.github.com/users/rails","html_url":"https://github.com/rails","followers_url":"https://api.github.com/users/rails/followers","following_url":"https://api.github.com/users/rails/following{/other_user}","gists_url":"https://api.github.com/users/rails/gists{/gist_id}","starred_url":"https://api.github.com/users/rails/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rails/subscriptions","organizations_url":"https://api.github.com/users/rails/orgs","repos_url":"https://api.github.com/users/rails/repos","events_url":"https://api.github.com/users/rails/events{/privacy}","received_events_url":"https://api.github.com/users/rails/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/rails/rails","description":"Ruby on Rails","fork":false,"url":"https://api.github.com/repos/rails/rails","forks_url":"https://api.github.com/repos/rails/rails/forks","keys_url":"https://api.github.com/repos/rails/rails/keys{/key_id}","collaborators_url":"https://api.github.com/repos/rails/rails/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/rails/rails/teams","hooks_url":"https://api.github.com/repos/rails/rails/hooks","issue_events_url":"https://api.github.com/repos/rails/rails/issues/events{/number}","events_url":"https://api.github.com/repos/rails/rails/events","assignees_url":"https://api.github.com/repos/rails/rails/assignees{/user}","branches_url":"https://api.github.com/repos/rails/rails/branches{/branch}","tags_url":"https://api.github.com/repos/rails/rails/tags","blobs_url":"https://api.github.com/repos/rails/rails/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/rails/rails/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/rails/rails/git/refs{/sha}","trees_url":"https://api.github.com/repos/rails/rails/git/trees{/sha}","statuses_url":"https://api.github.com/repos/rails/rails/statuses/{sha}","languages_url":"https://api.github.com/repos/rails/rails/languages","stargazers_url":"https://api.github.com/repos/rails/rails/stargazers","contributors_url":"https://api.github.com/repos/rails/rails/contributors","subscribers_url":"https://api.github.com/repos/rails/rails/subscribers","subscription_url":"https://api.github.com/repos/rails/rails/subscription","commits_url":"https://api.github.com/repos/rails/rails/commits{/sha}","git_commits_url":"https://api.github.com/repos/rails/rails/git/commits{/sha}","comments_url":"https://api.github.com/repos/rails/rails/comments{/number}","issue_comment_url":"https://api.github.com/repos/rails/rails/issues/comments/{number}","contents_url":"https://api.github.com/repos/rails/rails/contents/{+path}","compare_url":"https://api.github.com/repos/rails/rails/compare/{base}...{head}","merges_url":"https://api.github.com/repos/rails/rails/merges","archive_url":"https://api.github.com/repos/rails/rails/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/rails/rails/downloads","issues_url":"https://api.github.com/repos/rails/rails/issues{/number}","pulls_url":"https://api.github.com/repos/rails/rails/pulls{/number}","milestones_url":"https://api.github.com/repos/rails/rails/milestones{/number}","notifications_url":"https://api.github.com/repos/rails/rails/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/rails/rails/labels{/name}","releases_url":"https://api.github.com/repos/rails/rails/releases{/id}","created_at":"2008-04-11T02:19:47Z","updated_at":"2015-02-08T19:33:50Z","pushed_at":"2015-02-08T15:38:04Z","git_url":"git://github.com/rails/rails.git","ssh_url":"git@github.com:rails/rails.git","clone_url":"https://github.com/rails/rails.git","svn_url":"https://github.com/rails/rails","homepage":"http://rubyonrails.org","size":370337,"stargazers_count":24854,"watchers_count":24854,"language":"Ruby","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":9675,"mirror_url":null,"open_issues_count":752,"forks":9675,"open_issues":752,"watchers":24854,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/rails/rails/pulls/18824"},"html":{"href":"https://github.com/rails/rails/pull/18824"},"issue":{"href":"https://api.github.com/repos/rails/rails/issues/18824"},"comments":{"href":"https://api.github.com/repos/rails/rails/issues/18824/comments"},"review_comments":{"href":"https://api.github.com/repos/rails/rails/pulls/18824/comments"},"review_comment":{"href":"https://api.github.com/repos/rails/rails/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/rails/rails/pulls/18824/commits"},"statuses":{"href":"https://api.github.com/repos/rails/rails/statuses/3c1e227fc388d6874fb2981611c5ee9057704bd2"}}}}