{"id":14956019,"url":"https://github.com/owen2345/pub_sub_model_sync","last_synced_at":"2025-10-08T19:09:42.656Z","repository":{"id":38821594,"uuid":"246665520","full_name":"owen2345/pub_sub_model_sync","owner":"owen2345","description":"Permit to sync models and data between rails apps through pub/sub (google pubsub, rabbitmq, kafka)","archived":false,"fork":false,"pushed_at":"2023-03-09T02:25:01.000Z","size":1841,"stargazers_count":10,"open_issues_count":13,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-10-08T03:01:24.927Z","etag":null,"topics":["activerecord","bunny","google-pub","pubsub","rabbitmq","rails","ruby-on-rails","sync-models"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/owen2345.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-03-11T19:46:08.000Z","updated_at":"2025-09-26T09:39:42.000Z","dependencies_parsed_at":"2024-08-05T18:35:24.028Z","dependency_job_id":null,"html_url":"https://github.com/owen2345/pub_sub_model_sync","commit_stats":{"total_commits":387,"total_committers":3,"mean_commits":129.0,"dds":"0.023255813953488413","last_synced_commit":"85d76363cbec8999c44d17e9c700762eee7920df"},"previous_names":[],"tags_count":49,"template":false,"template_full_name":null,"purl":"pkg:github/owen2345/pub_sub_model_sync","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/owen2345%2Fpub_sub_model_sync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/owen2345%2Fpub_sub_model_sync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/owen2345%2Fpub_sub_model_sync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/owen2345%2Fpub_sub_model_sync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/owen2345","download_url":"https://codeload.github.com/owen2345/pub_sub_model_sync/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/owen2345%2Fpub_sub_model_sync/sbom","scorecard":{"id":715652,"data":{"date":"2025-08-11","repo":{"name":"github.com/owen2345/pub_sub_model_sync","commit":"85d76363cbec8999c44d17e9c700762eee7920df"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.5,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/24 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/release.yml:1","Warn: no topLevel permission defined: .github/workflows/ruby.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: MIT License: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:9: update your workflow using https://app.stepsecurity.io/secureworkflow/owen2345/pub_sub_model_sync/release.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ruby.yml:34: update your workflow using https://app.stepsecurity.io/secureworkflow/owen2345/pub_sub_model_sync/ruby.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ruby.yml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/owen2345/pub_sub_model_sync/ruby.yml/master?enable=pin","Warn: containerImage not pinned by hash: Dockerfile:1: pin your Docker image by updating ruby:2.6 to ruby:2.6@sha256:a79c8ddb7f3d3748427e2d3a45dcae6d42f1d80d9ae3b98959b3a27b220bf434","Warn: containerImage not pinned by hash: samples/app1/Dockerfile:1","Warn: containerImage not pinned by hash: samples/app2/Dockerfile:1","Info:   0 out of   1 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   2 third-party GitHubAction dependencies pinned","Info:   0 out of   3 containerImage dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 7 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"86 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-h47h-mwp9-c6q6","Warn: Project is vulnerable to: GHSA-4g8v-vg43-wpgf","Warn: Project is vulnerable to: GHSA-8xww-x3g3-6jcv","Warn: Project is vulnerable to: GHSA-fwhr-88qx-h9g7","Warn: Project is vulnerable to: GHSA-p84v-45xj-wwqj","Warn: Project is vulnerable to: GHSA-vfg9-r3fq-jvx4","Warn: Project is vulnerable to: GHSA-vfm5-rmrh-j26v","Warn: Project is vulnerable to: GHSA-x76w-6vjr-8xgj","Warn: Project is vulnerable to: GHSA-wwhv-wxv9-rpgw","Warn: Project is vulnerable to: GHSA-xp5h-f8jf-rc8q","Warn: Project is vulnerable to: GHSA-579w-22j4-4749","Warn: Project is vulnerable to: GHSA-76r7-hhxj-r776","Warn: Project is vulnerable to: GHSA-hq7p-j377-6v63","Warn: Project is vulnerable to: GHSA-8h22-8cf7-hq6g","Warn: Project is vulnerable to: GHSA-r4mg-4433-c7g3","Warn: Project is vulnerable to: GHSA-cr5q-6q9f-rq6q","Warn: Project is vulnerable to: GHSA-j6gc-792m-qgm2","Warn: Project is vulnerable to: GHSA-pj73-v5mw-pm9j","Warn: Project is vulnerable to: GHSA-23c2-gwp5-pxw9","Warn: Project is vulnerable to: GHSA-735f-pc8j-v9w8","Warn: Project is vulnerable to: GHSA-496j-2rq6-j6cc","Warn: Project is vulnerable to: GHSA-j3g3-5qv5-52mj","Warn: Project is vulnerable to: GHSA-2qc6-mcvw-92cw","Warn: Project is vulnerable to: GHSA-353f-x4gh-cqq8","Warn: Project is vulnerable to: GHSA-59gp-qqm7-cw4j","Warn: Project is vulnerable to: GHSA-5w6v-399v-w3cc","Warn: Project is vulnerable to: GHSA-cgx6-hpwq-fhv5","Warn: Project is vulnerable to: GHSA-crjr-9rc5-ghw8","Warn: Project is vulnerable to: GHSA-fq42-c5rg-92c2","Warn: Project is vulnerable to: GHSA-gx8x-g87m-h5q6","Warn: Project is vulnerable to: GHSA-jc36-42cf-vqwj","Warn: Project is vulnerable to: GHSA-mrxw-mxhj-p664","Warn: Project is vulnerable to: GHSA-pxvg-2qj5-37jq","Warn: Project is vulnerable to: GHSA-r95h-9x8f-r3f7","Warn: Project is vulnerable to: GHSA-v6gp-9mmm-c6p5","Warn: Project is vulnerable to: GHSA-vvfq-8hwr-qm4m","Warn: Project is vulnerable to: GHSA-xc9x-jj77-9p9j","Warn: Project is vulnerable to: GHSA-xh29-r2w5-wx8m","Warn: Project is vulnerable to: GHSA-xxx9-3xcr-gjj3","Warn: Project is vulnerable to: GHSA-22f2-v57c-j9cx","Warn: Project is vulnerable to: GHSA-3h57-hmj3-gj3p","Warn: Project is vulnerable to: GHSA-54rr-7fvw-6x8f","Warn: Project is vulnerable to: GHSA-65f5-mfpf-vfhj","Warn: Project is vulnerable to: GHSA-7g2v-jj9q-g3rg","Warn: Project is vulnerable to: GHSA-7wqh-767x-r66v","Warn: Project is vulnerable to: GHSA-8cgq-6mh2-7j6v","Warn: Project is vulnerable to: GHSA-93pm-5p5f-3ghx","Warn: Project is vulnerable to: GHSA-c6qg-cjj8-47qp","Warn: Project is vulnerable to: GHSA-gjh7-p2fx-99vx","Warn: Project is vulnerable to: GHSA-rqv2-275x-2jq5","Warn: Project is vulnerable to: GHSA-vpfw-47h7-xj4g","Warn: Project is vulnerable to: GHSA-xj5v-6v4g-jfw6","Warn: Project is vulnerable to: GHSA-2rxp-v6pw-ch6m","Warn: Project is vulnerable to: GHSA-4xqq-m2hx-25v8","Warn: Project is vulnerable to: GHSA-5866-49gr-22v4","Warn: Project is vulnerable to: GHSA-r55c-59qm-vjw6","Warn: Project is vulnerable to: GHSA-vg3r-rm7w-2xgh","Warn: Project is vulnerable to: GHSA-vmwr-mc7x-5vc3","Warn: Project is vulnerable to: GHSA-2rqw-v265-jf8c","Warn: Project is vulnerable to: GHSA-mm33-5vfq-3mm3","Warn: Project is vulnerable to: GHSA-qphc-hf5q-v8fc","Warn: Project is vulnerable to: GHSA-wh98-p28r-vrc9","Warn: Project is vulnerable to: GHSA-ch3h-j2vf-95pv","Warn: Project is vulnerable to: GHSA-3hhc-qp5v-9p2j","Warn: Project is vulnerable to: GHSA-w749-p3v6-hccq","Warn: Project is vulnerable to: GHSA-228g-948r-83gx","Warn: Project is vulnerable to: GHSA-3x8r-x6xp-q4vm","Warn: Project is vulnerable to: GHSA-486f-hjj9-9vhh","Warn: Project is vulnerable to: GHSA-286v-pcf5-25rc","Warn: Project is vulnerable to: GHSA-2rr5-8q37-2w7h","Warn: Project is vulnerable to: GHSA-7rrm-v45f-jp64","Warn: Project is vulnerable to: GHSA-jw9f-hh49-cvp9","Warn: Project is vulnerable to: GHSA-v4f8-2847-rwm7","Warn: Project is vulnerable to: GHSA-48w2-rm65-62xx","Warn: Project is vulnerable to: GHSA-68xg-gqqm-vgj8","Warn: Project is vulnerable to: GHSA-9hf4-67fc-4vf4","Warn: Project is vulnerable to: GHSA-c2f4-cvqm-65w2","Warn: Project is vulnerable to: GHSA-h99w-9q5r-gjq9","Warn: Project is vulnerable to: GHSA-rmj8-8hhh-gv5h","Warn: Project is vulnerable to: GHSA-hxqx-xwvh-44m2","Warn: Project is vulnerable to: GHSA-wq4h-7r42-5hrr","Warn: Project is vulnerable to: GHSA-5x79-w82f-gw8w","Warn: Project is vulnerable to: GHSA-9h9g-93gc-623h","Warn: Project is vulnerable to: GHSA-mcvf-2q2m-x72m","Warn: Project is vulnerable to: GHSA-pg8v-g4xq-hww9","Warn: Project is vulnerable to: GHSA-rrfc-7g8p-99q8"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-22T09:34:19.628Z","repository_id":38821594,"created_at":"2025-08-22T09:34:19.628Z","updated_at":"2025-08-22T09:34:19.628Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279000701,"owners_count":26082805,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","bunny","google-pub","pubsub","rabbitmq","rails","ruby-on-rails","sync-models"],"created_at":"2024-09-24T13:12:11.573Z","updated_at":"2025-10-08T19:09:42.639Z","avatar_url":"https://github.com/owen2345.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# **PubSubModelSync**\n![Rails badge](https://img.shields.io/badge/Rails-4+-success.png)\n![Ruby badge](https://img.shields.io/badge/Ruby-2.4+-success.png)\n![Production badge](https://img.shields.io/badge/Production-ready-success.png)\n\nThis gem permits to sync automatically models and custom data between multiple Rails applications by publishing notifications via pubsub (Google PubSub, RabbitMQ, or Apache Kafka) and automatically processed by all connected applications. Out of the scope, this gem includes transactions to keep Data consistency by processing notifications in the order they were delivered.     \nThese notifications use JSON format to easily be decoded by subscribers (Rails applications and even other languages, soon for [Cristal-lang](https://crystal-lang.org/)) \n\n- [**PubSubModelSync**](#pubsubmodelsync)\n  - [**Features**](#features)\n  - [**Installation**](#installation)\n  - [**Configuration**](#configuration)\n  - [**Notifications Diagram**](#notifications-diagram)\n  - [**Examples**](#examples)\n    - [**Basic Example**](#basic-example)\n    - [**Advanced Example**](#advanced-example)\n  - [**API**](#api)\n    - [**Subscribers**](#subscribers)\n      - [**Registering Subscriptions**](#registering-subscriptions)\n      - [**Subscription helpers**](#subscription-helpers)\n    - [**Publishers**](#publishers)\n      - [**Publishing notifications**](#publishing-notifications)\n      - [**Publisher Helpers**](#publisher-helpers)\n      - [**Publisher callbacks**](#publisher-callbacks)\n    - [**Payload**](#payload)\n  - [**Transactions**](#transactions)\n  - [**Testing with RSpec**](#testing-with-rspec)\n  - [**Extra configurations**](#extra-configurations)\n  - [**TODO**](#todo)\n  - [**Q\u0026A**](#qa)\n  - [**Contributing**](#contributing)\n  - [**License**](#license)\n  - [**Code of Conduct**](#code-of-conduct)\n\n## **Features**\n- Sync model data between Rails apps: All changes made on App1, will be immediately reflected on App2, App3, etc.    \n    Example: If User is created on App1, this user will be created on App2, App3 too with the accepted attributes.\n- Ability to send instance and class level notifications    \n    Example: If App1 wants to send emails to multiple users, this can be listened on App2, to deliver corresponding emails\n- Change pub/sub service at any time: Switch between rabbitmq, kafka, google pubsub  \n- Support for transactions: Permits to keep data consistency between applications by processing notifications in the same order they were delivered (auto included in models transactions).\n- Ability to send notifications to a specific topic (single application) or multiple topics (multiple applications)\n\n## **Installation**\nAdd this line to your application's Gemfile:\n```ruby\ngem 'pub_sub_model_sync', '\u003e= 1.9.3'\n\ngem 'google-cloud-pubsub', '\u003e= 2.14.0' # to use google pub/sub service. For old rails apps ('\u003e= 1.9', '\u003c= 2.9.2')\ngem 'bunny' # to use rabbit-mq pub/sub service\ngem 'ruby-kafka' # to use apache kafka pub/sub service\n```\nAnd then execute: $ bundle install\n\n\n## **Configuration**\n\n- Configuration for google pub/sub (You need google pub/sub service account)\n    ```ruby\n    # initializers/pub_sub_config.rb\n    PubSubModelSync::Config.service_name = :google\n    PubSubModelSync::Config.project = 'google-project-id'\n    PubSubModelSync::Config.credentials = 'path-to-google-config.json'\n    PubSubModelSync::Config.topic_name = 'sample-topic' \n    PubSubModelSync::Config.subscription_name = 'my-app1'\n    ```\n    See details here:\n    https://github.com/googleapis/google-cloud-ruby/tree/master/google-cloud-pubsub\n\n- configuration for RabbitMq (You need rabbitmq installed)\n    ```ruby\n    PubSubModelSync::Config.service_name = :rabbitmq\n    PubSubModelSync::Config.bunny_connection = 'amqp://guest:guest@localhost'\n    PubSubModelSync::Config.topic_name = 'sample-topic'\n    PubSubModelSync::Config.subscription_name = 'my-app2'\n    ```\n    See details here: https://github.com/ruby-amqp/bunny\n\n- configuration for Apache Kafka (You need kafka installed)\n    ```ruby\n    PubSubModelSync::Config.service_name = :kafka\n    PubSubModelSync::Config.kafka_connection = [[\"kafka1:9092\", \"localhost:2121\"], { logger: Rails.logger }]\n    PubSubModelSync::Config.topic_name = 'sample-topic'\n    PubSubModelSync::Config.subscription_name = 'my-app3'\n    ```\n    See details here: https://github.com/zendesk/ruby-kafka    \n\n  *Important: The `topic_name` must be the same for all applications, so that, the apps connect to the same topic*\n\n- Add publishers/subscribers to your models (See examples below)\n\n- Start subscribers to listen for publishers (Only in the app that has subscribers)\n    ```bash\n      DB_POOL=20 bundle exec rake pub_sub_model_sync:start\n    ```\n    Note: You need more than 15 DB pools to avoid \"could not obtain a connection from the pool within 5.000 seconds\". https://devcenter.heroku.com/articles/concurrency-and-database-connections\n\n- Check the service status with:\n  ```ruby\n    PubSubModelSync::Payload.new({ my_data: 'here' }, { klass: 'MyClass', action: :sample_action }).publish!\n  ```\n\n- More configurations: [here](#extra-configurations)\n\n## **Notifications Diagram**\n![Diagram](/docs/notifications-diagram.png?raw=true)\n\n## **Examples**\nSee sample apps in [/samples](/samples/)   \n### **Basic Example**\n```ruby\n# App 1 (Publisher)\nclass User \u003c ActiveRecord::Base\n  include PubSubModelSync::PublisherConcern\n  ps_after_action(:create) { ps_publish(:create, mapping: %i[id name email]) }\n  ps_after_action(:update) { ps_publish(:update, mapping: %i[id name email]) }\n  ps_after_action(:destroy) { ps_publish(:destroy, mapping: %i[id]) }  \nend\n\n# App 2 (Subscriber)\nclass User \u003c ActiveRecord::Base\n  include PubSubModelSync::SubscriberConcern\n  ps_subscribe([:create, :update, :destroy], %i[name email], id: :id) # crud notifications\nend\n\n# CRUD syncs\nmy_user = User.create!(name: 'test user', email: 'sample@gmail.com') # Publishes `:create` notification (App 2 syncs the new user)\nmy_user.update!(name: 'changed user') # Publishes `:update` notification (App2 updates changes on user with the same id)\nmy_user.destroy! # Publishes `:destroy` notification (App2 destroys the corresponding user)\n```\n\n### **Advanced Example**\n```ruby\n# App 1 (Publisher)\nclass User \u003c ActiveRecord::Base\n  include PubSubModelSync::PublisherConcern\n  ps_after_action([:create, :update]) do |action| \n    ps_publish(action, mapping: %i[name:full_name email], as_klass: 'App1User', headers: { topic_name: %i[topic1 topic2] })\n  end\nend\n\n# App 2 (Subscriber)\nclass User \u003c ActiveRecord::Base\n  include PubSubModelSync::SubscriberConcern\n  ps_subscribe([:create, :update], %i[full_name:customer_name], id: :email, from_klass: 'App1User')  \n  ps_subscribe(:send_welcome, %i[email], id: :email, to_action: :send_email, if: -\u003e(model) { model.email.present? })\n  ps_class_subscribe(:batch_disable) # class subscription\n  \n  def send_email\n    puts \"sending email to #{email}\"\n  end\n  \n  def self.batch_disable(data)\n    puts \"disabling users: #{data[:ids]}\"\n  end\nend\nmy_user = User.create!(name: 'test user', email: 's@gmail.com') # Publishes `:create` notification with classname `App1User` (App2 syncs the new user)\nmy_user.ps_publish(:send_welcome, mapping: %i[id email]) # Publishes `:send_welcome` notification (App2 prints \"sending email to...\")\nPubSubModelSync::Payload.new({ ids: [my_user.id] }, { klass: 'User', action: :batch_disable, mode: :klass }).publish! # Publishes class notification (App2 prints \"disabling users..\")\n```\n\n## **API**\n### **Subscribers**\n\n#### **Registering Subscriptions**\n```ruby\n  class MyModel \u003c ActiveRecord::Base\n    ps_subscribe(action, mapping, settings)\n    ps_class_subscribe(action, settings)\n  end\n  ```\n- Instance subscriptions: `ps_subscribe(action, mapping, settings, \u0026block)`     \n  When model receives the corresponding notification, `action` or `to_action` method will be called on the model. Like: `model.destroy`\n  - `action` (Symbol|Array\u003cSymbol\u003e) Only notifications with this action name will be processed by this subscription. Sample: save|create|update|destroy|\u003cany_other_action\u003e    \n  - `mapping` (Array\u003cString\u003e) Data mapping from payload data into model attributes, sample: [\"email\", \"full_name:name\"] (Note: Only these attributes will be assigned/synced to the current model)    \n    - `[email]` means that `email` value from payload will be assigned to `email` attribute from current model \n    - `[full_name:name]` means that `full_name` value from payload will be assigned to `name` attribute from current model \n  - `settings` (Hash\u003c:from_klass, :to_action, :id, :if, :unless\u003e)    \n    - `from_klass:` (String, default current class): Only notifications with this class name will be processed by this subscription    \n    - `to_action:` (Symbol|Proc, default `action`):        \n      When Symbol: Model method to process the notification, sample: `def my_method(data)...end`    \n      When Proc: Block to process the notification, sample: `{|data| ... }`    \n    - `id:` (Symbol|Array\u003cSymbol|String\u003e, default: `:id`) identifier attribute(s) to find the corresponding model instance (Supports for mapping format)    \n      Sample: `id: :id` will search for a model like: `model_class.where(id: payload.data[:id])`       \n      Sample: `id: [:id, :email:user_email]` will search for a model like: `model_class.where(id: payload.data[:id], user_email: payload.data[:email])`       \n    - `if:` (Symbol|Proc|Array\u003cSymbol\u003e) Method(s) or block called for the confirmation before calling the callback    \n    - `unless:` (Symbol|Proc|Array\u003cSymbol\u003e) Method or block called for the negation before calling the callback\n  - `\u0026block` Block to be used as the callback method (ignored if `:to_action` is present). Sample: `ps_subscribe(:send_welcome, %i[email]) { |_data| puts model.email }`    \n\n- Class subscriptions: `ps_class_subscribe(action, settings, \u0026block)`     \n  When current class receives the corresponding notification, `action` or `to_action` method will be called on the Class. Like: `User.hello(data)`\n  * `action` (Symbol) Notification.action name\n  * `settings` (Hash) refer ps_subscribe.settings except(:id)\n  * `\u0026block` Block to be used as the callback method (ignored if `:to_action` is present). Sample: `ps_class_subscribe(:send_welcome) { |data| puts data }`\n\n- `ps_processing_payload` a class and instance variable that saves the current payload being processed\n\n- (Only instance subscription) Perform custom actions before saving sync of the model (`:cancel` can be returned to skip sync)\n  ```ruby\n  class MyModel \u003c ActiveRecord::Base\n    def ps_before_save_sync\n      # puts ps_processing_payload.data[:id]\n    end\n  end\n  ```\n  \n- (Only instance subscription) Configure a custom model finder (optional)\n  ```ruby\n  class MyModel \u003c ActiveRecord::Base\n    def ps_find_model(data)\n      where(custom_finder: data[:custom_value]).first_or_initialize\n    end\n  end\n  ```\n  * `data`: (Hash) Payload data received from sync\n  Must return an existent or a new model object\n\n#### **Subscription helpers**\n- List all configured subscriptions\n  ```ruby\n    PubSubModelSync::Config.subscribers\n  ```\n- Process or reprocess a notification\n  ```ruby\n    payload = PubSubModelSync::Payload.new(data, attributes, headers)\n    payload.process!\n  ```\n\n\n### **Publishers**\n```ruby\n  class MyModel \u003c ActiveRecord::Base\n    ps_after_action([:create, :update, :destroy], :method_publisher_name) # using method callback\n    ps_after_action([:create, :update, :destroy]) do |action| # using block callback\n      ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)\n      ps_class_publish({}, action: :my_action, as_klass: nil, headers: {})\n    end\n\n    def method_publisher_name(action)\n      ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)\n    end\n  end\n  ```\n\n#### **Publishing notifications**\n\n- `ps_after_action(crud_actions, method_name = nil, \u0026block)` Listens for CRUD events and calls provided `block` or `method` to process event callback\n  - `crud_actions` (Symbol|Array\u003cSymbol\u003e) Crud event(s) to be observed (Allowed: `:create, :update, :destroy`)\n  - `method_name` (Symbol, optional) method to be called to process action callback, sample: `def my_method(action) ... end`\n  - `block` (Proc, optional) Block to be called to process action callback, sample: `{ |action| ... }`     \n  \n  **Note1**: Due to rails callback ordering, this method uses `after_commit on: action {...}` callback when creating or updating models to ensure expected notifications order (More details [**here**](#transactions)).    \n  **Note2**: Due to rails callback ordering, this method uses `after_destroy` callback when destroying models to ensure the expected notifications order.    \n   \n- `ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: nil)` Delivers an instance notification via pubsub\n  - `action` (Sym|String) Action name of the instance notification. Sample: create|update|destroy|\u003cany_other_key\u003e\n  - `mapping:` (Array\u003cString\u003e, optional) Generates payload data using the provided mapper:\n      - Sample: `[\"id\", \"name\"]` will result into `{ id: \u003cmodel.id\u003e,  name: \u003cmodel.name\u003e}` \n      - Sample: `[\"id\", \"full_name:name\"]` will result into `{ id: \u003cmodel.id\u003e,  name: \u003cmodel.full_name\u003e}`\n  - `data:` (Hash|Symbol|Proc, optional)\n    - When Hash: Data to be added to the final payload\n    - When Symbol: Method name to be called to retrieve payload data (must return a `hash`, receives `:action` as arg)\n    - When Proc: Block to be called to retrieve payload data (must return a `hash`, receives `:model, :action` as args)\n  - `headers:` (Hash|Symbol|Proc, optional): Defines how the notification will be delivered and be processed (All available attributes in Payload.headers)\n    - When Hash: Data that will be merged with default header values\n    - When Symbol: Method name that will be called to retrieve header values (must return a hash, receives `:action` arg)\n    - When Proc: Block to be called to retrieve header values (must return a `hash`, receives `:model, :action` as args)\n  - `as_klass:` (String, default current class name): Output class name used instead of current class name\n  \n- `ps_class_publish(data, action:, as_klass: nil, headers: {})` Delivers a  Class notification via pubsub\n  - `data` (Hash): Data of the notification\n  - `action` (Symbol): action  name of the notification\n  - `as_klass:` (String, default current class name): Class name of the notification\n  - `headers:` (Hash, optional): header settings (More in Payload.headers)\n\n- `ps_perform_publish(action = :create, parents_actions: false)` Permits to perform manually the callback of a specific `ps_after_action`\n  - `action` (Symbol, default: :create) Only :create|:update|:destroy\n  - `parents_actions` (Boolean, default: false) When `true`, includes inherited PubSub-callbacks from parent classes\n    \n  \n#### **Publisher helpers**\n- Publish or republish a notification\n  ```ruby\n    payload = PubSubModelSync::Payload.new(data, attributes, headers)\n    payload.publish!\n  ```\n\n#### **Publisher callbacks**\n\n- Do some actions before publishing notification.\n  If returns \":cancel\", notification will not be delivered\n  ```ruby\n      class MyModel \u003c ActiveRecord::Base\n        def ps_before_publish(action, payload)\n          # logic here\n        end\n      end\n  ```\n\n- Do some actions after notification was delivered.\n  ```ruby\n    class MyModel \u003c ActiveRecord::Base\n      def ps_after_publish(action, payload)\n        # logic here\n      end\n    end\n  ```\n\n\n### **Payload**\nAny notification before delivering is transformed as a Payload for a better portability. \n\n- Attributes  \n  * `data`: (Hash) Data to be published or processed\n  * `info`: (Hash) Notification info\n    - `action`: (String) Notification action name\n    - `klass`: (String) Notification class name\n    - `mode`: (Symbol: `:model`|`:class`) Kind of notification\n  * `headers`: (Hash) Notification settings that defines how the notification will be processed or delivered. \n    - `ordering_key`: (String, optional): notifications with the same `ordering_key` are processed in the same order they were delivered, default: `\u003cmodel.class.name\u003e/\u003cmodel.id\u003e` when instance notification and `klass_name` when class notification.    \n      Note: Final `ordering_key` is calculated as: `payload.headers[:forced_ordering_key] || current_transaction\u0026.key || payload.headers[:ordering_key]`         \n    - `topic_name`: (String|Array\u003cString\u003e, optional): Specific topic name where to deliver the notification (default `PubSubModelSync::Config.topic_name`).\n    - `forced_ordering_key`: (String, optional): Overrides `ordering_key` with the provided value even withing transactions. Default `nil` (if `true`, prevails the payload's `ordering_key`).\n    - `target_app_key`: (String, optional): Allows to send the notification to a specific app (includes the application key, separated by comma when multiple apps). Default `nil`.\n    - `cache` (Boolean | Hash, Default false) Cache settings   \n        - `true`: Skip publishing similar payloads\n        - `Hash\u003crequired: Array\u003cSymbol\u003e\u003e`: Same as `true` and enables payload optimization to exclude unchanged non important attributes. Sample: `{ required: %i[id email] }`\n    \n    ** Read ONLY **    \n    - `internal_key`: Internal identifier of the payload, default: `\u003cmodel.class.name\u003e/\u003caction\u003e/\u003cmodel.id\u003e` when model notification and `\u003cklass_name\u003e/\u003caction\u003e` when class notification (Useful for caching techniques).\n    - `app_key`: (Auto calculated): Name of the application who delivered the notification.\n    - `uuid`: (Auto calculated): Unique notification identifier (Very useful when debugging).   \nNote: To reduce Payload size, some header info are not delivered (Enable debug mode to deliver all payload info). \n  \n- Actions\n  ```ruby\n    payload.publish! # publishes notification data. It raises exception if fails and does not call ```:on_error_publishing``` callback\n    payload.publish # publishes notification data. On error does not raise exception but calls ```:on_error_publishing``` callback\n    payload.process! # process a notification data. It raises exception if fails and does not call ```.on_error_processing``` callback\n    payload.process # process a notification data. It does not raise exception if fails but calls ```.on_error_processing``` callback\n    payload.retry_publish! # allows to retry publishing a failed payload (All callbacks are ignored)\n  ```\n\n## **Transactions**   \n  This Gem supports to publish multiple notifications to be processed in the same order they are published.   \n  * Crud syncs auto includes transactions which works as the following:\n    ```ruby\n    class User\n      ps_after_action([:create, :update, :destroy]) { |action| ps_publish(action, mapping: %i[id name]) }\n      has_many :posts, dependent: :destroy\n      accepts_nested_attributes_for :posts\n    end\n    \n    class Post\n      belongs_to :user\n      ps_after_action([:create, :update, :destroy]) { |action| ps_publish(action, mapping: %i[id user_id title]) }\n    end\n    ```\n    - When created (all notifications use the same ordering_key to be processed in the same order)\n      ```ruby\n        user = User.create!(name: 'test', posts_attributes: [{ title: 'Post 1' }, { title: 'Post 2' }])\n        # notification #1 =\u003e \u003cPayload data: {id: 1, name: 'sample'}, info: { klass: 'User', action: :create, mode: :model }, headers: { ordering_key = `User/1` }\u003e\n        # notification #2 =\u003e \u003cPayload data: {id: 1, title: 'Post 1', user_id: 1}, info: { klass: 'Post', action: :create, mode: :model }, headers: { ordering_key = `User/1` }\u003e\n        # notification #3 =\u003e \u003cPayload data: {id: 2, title: 'Post 2', user_id: 1}, info: { klass: 'Post', action: :create, mode: :model }, headers: { ordering_key = `User/1` }\u003e\n      ```\n    - When updated (all notifications use the same ordering_key to be processed in the same order)\n      ```ruby\n        user.update!(name: 'changed', posts_attributes: [{ id: 1, title: 'Post 1C' }, { id: 2, title: 'Post 2C' }])\n        # notification #1 =\u003e \u003cPayload data: {id: 1, name: 'changed'}, info: { klass: 'User', action: :update, mode: :model }, headers: { ordering_key = `User/1` }\u003e\n        # notification #2 =\u003e \u003cPayload data: {id: 1, title: 'Post 1C', user_id: 1}, info: { klass: 'Post', action: :update, mode: :model }, headers: { ordering_key = `User/1` }\u003e\n        # notification #3 =\u003e \u003cPayload data: {id: 2, title: 'Post 2C', user_id: 1}, info: { klass: 'Post', action: :update, mode: :model }, headers: { ordering_key = `User/1` }\u003e\n      ```\n    - When destroyed (all notifications use the same ordering_key to be processed in the same order)   \n      **Note**: The notifications order were reordered in order to avoid inconsistency in other apps \n      ```ruby\n      user.destroy!\n      # notification #1 =\u003e \u003cPayload data: {id: 1, title: 'Post 1C', user_id: 1}, info: { klass: 'Post', action: :destroy, mode: :model }\u003e\n      # notification #2 =\u003e \u003cPayload data: {id: 2, title: 'Post 2C', user_id: 1}, info: { klass: 'Post', action: :destroy, mode: :model }\u003e\n      # notification #3 =\u003e \u003cPayload data: {id: 1, name: 'changed'}, info: { klass: 'User', action: :destroy, mode: :model }\u003e\n      ```\n    By this way parent notification and all inner notifications are processed in the same order they were published (includes notifications from callbacks like `ps_before_publish`).\n        \n    **Note**: When any error is raised when saving user or posts, the transaction is cancelled and thus all notifications wont be delivered (customizable by `PubSubModelSync::Config.transactions_use_buffer`).    \n  \n  - Manual transactions   \n    `PubSubModelSync::MessagePublisher::transaction(key, headers: { target_app_key: 'my_other_app_key' } \u0026block)`\n    - `key` (String|nil) Key used as the ordering_key for all inner notifications (When nil, will use `ordering_key` of the first notification)\n    - `headers:` (Hash) Header settings to be added to each Payload's header inside this transaction     \n    Sample:\n    ```ruby\n      PubSubModelSync::MessagePublisher::transaction('my-custom-key', headers: { key: 'my-key' }) do\n        user = User.create(name: 'test') # `User`:`:create` notification\n        post = Post.create(title: 'sample') # `Post`:`:create` notification\n        PubSubModelSync::Payload.new({ ids: [user.id] }, { klass: 'User', action: :send_welcome, mode: :klass }).publish! # `User`:`:send_welcome` notification\n      end\n    ```\n    All notifications uses `ordering_key: 'my-custom-key'` and will be processed in the same order they were published (Payload headers will include `key=\"my-key\"`).\n\n## **Testing with RSpec**\n- Config: (spec/rails_helper.rb)\n  ```ruby\n      config.before(:each) do\n        # disable delivering notifications to pubsub\n        allow(PubSubModelSync::MessagePublisher).to receive(:connector_publish)\n        # disable all models sync by default (reduces testing time by avoiding to build payload data)\n        allow(PubSubModelSync::MessagePublisher).to receive(:publish_model)\n      end\n    \n      # enable all models sync only for tests that includes 'sync: true'\n      config.before(:each, sync: true) do\n        allow(PubSubModelSync::MessagePublisher).to receive(:publish_model).and_call_original\n      end\n      \n      # Only when using database cleaner in old versions of rspec (enables after_commit callback)\n      # config.before(:each, truncate: true) do\n      #   DatabaseCleaner.strategy = :truncation\n      # end\n  ```\n- Examples:\n  - **Publisher**    \n    Note: **Do not forget to include 'sync: true'** to enable publishing pubsub notifications\n    ```ruby\n      describe 'When publishing sync', truncate: true, sync: true do\n        it 'publishes user notification when created' do\n          expect_publish_notification(:create, klass: 'User')\n          create(:user)\n        end\n        \n        it 'publishes user notification with all defined data' do\n          user = build(:user)\n          data = PubSubModelSync::PayloadBuilder.parse_mapping_for(user, %i[id name:full_name email])\n          data[:id] = be_a(Integer)\n          expect_publish_notification(:create, klass: 'User', data: data)\n          user.save!\n        end\n        \n        it 'publishes user notification when created' do\n          email = 'Newemail@gmail.com'\n          user = create(:user)\n          expect_publish_notification(:update, klass: 'User', data: { id: user.id, email: email })\n          user.update!(email: email)\n        end\n        \n        it 'publishes user notification when created' do\n          user = create(:user)\n          expect_publish_notification(:destroy, klass: 'User', data: { id: user.id })\n          user.destroy!\n        end\n        \n        private\n        \n        # @param action (Symbol)\n        # @param klass (String, default described_class name)\n        # @param data (Hash, optional) notification data\n        # @param info (Hash, optional) notification info\n        # @param headers (Hash, optional) notification headers\n        def expect_publish_notification(action, klass: described_class.to_s, data: {}, info: {}, headers: {})\n          publisher = PubSubModelSync::MessagePublisher\n          exp_data = have_attributes(data: hash_including(data),\n                                     info: hash_including(info.merge(klass: klass, action: action)),\n                                     headers: hash_including(headers))\n          allow(publisher).to receive(:publish!).and_call_original\n          expect(publisher).to receive(:publish!).with(exp_data)\n        end\n      end\n    ```   \n  - **Subscriber**    \n  ```ruby\n  \n  describe 'when syncing data from other apps' do\n    it 'creates user when received :create notification' do\n      user = build(:user)\n      data = user.as_json(only: %i[name email]).merge(id: 999)\n      payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :create })\n      expect { payload.process! }.to change(described_class, :count)\n    end\n\n    it 'updates user when received :update notification' do\n      user = create(:user)\n      name = 'new name'\n      data = user.as_json(only: %i[id email]).merge(name: name)\n      payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :update })\n      payload.process!\n      expect(user.reload.name).to eq(name)\n    end\n\n    it 'destroys user when received :destroy notification' do\n      user = create(:user)\n      data = user.as_json(only: %i[id])\n      payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: :destroy })\n      payload.process!\n      expect { user.reload }.to raise_error(ActiveRecord::RecordNotFound)\n    end\n  \n    \n    it 'receive custom model notification' do\n      user = create(:user)  \n      data = { id: user.id, custom_data: {} }\n      custom_action = :say_hello\n      expect_any_instance_of(User).to receive(custom_action).with(data)\n      payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: custom_action })\n      payload.process!\n    end\n\n    it 'receive class notification' do\n      data = { msg: 'hello' }\n      action = :greeting\n      expect(User).to receive(action).with(data)\n      # Do not forget to include `mode: :klass` for class notifications\n      payload = PubSubModelSync::Payload.new(data, { klass: 'User', action: action, mode: :klass })\n      payload.process!\n    end\n  end\n  ```\n\n## **Extra configurations**\n```ruby\nconfig = PubSubModelSync::Config\nconfig.debug = true\n```\n- `.topic_name = ['topic1', 'topic 2']`: (String|Array\u003cString\u003e)    \n    Topic name(s) to be used to listen all notifications from when listening. Additionally first topic name is used as the default topic name when publishing a notification. \n- `.subscription_name = \"my-app-1\"`:  (String, default Rails.application.name)     \n    Subscriber's identifier which helps to: \n    * skip self notifications\n    * continue the sync from the last synced notification when service was restarted.\n- `.default_topic_name = \"my_topic\"`: (String|Array\u003cString\u003e, optional(default first topic from `topic_name`))     \n    Topic name used as the default topic if not defined in the payload when publishing a notification\n- ```.debug = true```\n    (true/false*) =\u003e show advanced log messages\n- ```.logger = Rails.logger```\n    (Logger) =\u003e define custom logger\n- ```.on_before_processing = -\u003e(payload, {subscriber:}) { puts payload }```\n    (Proc) =\u003e called before processing a received notification (:cancel can be returned to skip processing)\n- ```.on_success_processing = -\u003e(payload, {subscriber:}) { puts payload }```\n    (Proc) =\u003e called when a notification was successfully processed\n- ```.on_error_processing = -\u003e(exception, {payload:, subscriber:}) { payload.delay(...).process! }```\n    (Proc) =\u003e called when a notification has failed when processing (delayed_job or similar can be used for retrying)\n- ```.on_before_publish = -\u003e(payload) { puts payload }```\n    (Proc) =\u003e called before publishing a notification (:cancel can be returned to skip publishing)\n- ```.on_after_publish = -\u003e(payload) { puts payload }```\n    (Proc) =\u003e called after publishing a notification\n- ```.on_error_publish = -\u003e(exception, {payload:}) { payload.delay(...).retry_publish! }```\n    (Proc) =\u003e called when failed publishing a notification (delayed_job or similar can be used for retrying)\n- ```.skip_cache = false```\n  (true/false*) =\u003e Allow to skip payload optimization (cache settings)\n- ```.sync_mode = true```\n    (true/false*) =\u003e If `true`, the messages are delivered synchronously, else, they are delivered asynchronously (Currently, only GooglePubsub supports it). Also it can be enabled via env var: `PUBSUB_MODEL_SYNC_MODE=true`\n- ```.transactions_max_buffer = 1``` (Integer, default 1) Controls the maximum quantity of notifications to be enqueued to the transaction-buffer before delivering them and thus adds the ability to rollback notifications if the transaction fails.        \n    Once this quantity of notifications is reached, then all notifications of the current transaction will immediately be delivered (can be customized per transaction).    \n    Note: There is no way to rollback delivered notifications if current transaction fails later.      \n    Note2: Only notifications from the buffer can be rollbacked if the current transaction has failed.     \n\n## **TODO**\n- add the ability to raise SKIP_ACKNOWLEDGE to auto retry by PubSub\n- Auto publish update only if payload has changed (see ways to compare previous payload vs new payload)\n- Improve transactions to exclude similar notifications by klass and action. Sample:\n    ```PubSubModelSync::MessagePublisher.transaction(key, { same_keys: :use_last_as_first|:use_last|:use_first_as_last|:keep*, same_data: :use_last_as_first*|:use_last|:use_first_as_last|:keep })```\n- Add DB table to use as a shield to prevent publishing similar notifications and publish partial notifications (similar idea when processing notif)\n- Last notification is not being delivered immediately in google pubsub (maybe force with timeout 10secs and service.deliver_messages)\n- Update folder structure\n- Services support to deliver multiple payloads from transactions\n- Fix deprecation warnings: pub_sub_model_sync/service_google.rb:39: warning: Splitting the last argument into positional and keyword parameters is deprecated\n- Add if/unless to ps_after_action\n- Add subscription liveness checker using thread without db connection to check periodically pending notifications from google pubsub\n- Unify .stop() and 'Listener stopped' \n- TODO: Publish new version 1.2.1 (improve logs)\n- Enable `async` mode for rabbitMQ and Kafka\n\n## **Q\u0026A**\n- I'm getting error \"could not obtain a connection from the pool within 5.000 seconds\"... what does this mean?\n  This problem occurs because pub/sub dependencies (kafka, google-pubsub, rabbitmq) uses many threads to perform notifications where the qty of threads is greater than qty of DB pools ([Google pubsub info](https://github.com/googleapis/google-cloud-ruby/blob/master/google-cloud-pubsub/lib/google/cloud/pubsub/subscription.rb#L888))\n  To fix the problem, edit config/database.yml and increase the quantity of ```pool: ENV['DB_POOL'] || 5``` and `DB_POOL=20 bundle exec rake pub_sub_model_sync:start`\n- How to retry failed syncs with sidekiq?\n  ```ruby\n    # lib/initializers/pub_sub_config.rb\n\n    class PubSubRecovery\n      include Sidekiq::Worker\n      sidekiq_options queue: :pubsub, retry: 2, backtrace: true\n\n      def perform(payload_data, action)\n        payload = PubSubModelSync::Payload.from_payload_data(payload_data)\n        payload.send(action)\n      end\n    end\n\n    PubSubModelSync::Config.on_error_publish = lambda do |_e, data|\n      PubSubRecovery.perform_async(data[:payload].to_h, :retry_publish!)\n    end\n    PubSubModelSync::Config.on_error_processing = lambda do |_e, data|\n      PubSubRecovery.perform_async(data[:payload].to_h, :process!)\n    end\n  ```\n\n## **Contributing**\n\nBug reports and pull requests are welcome on GitHub at https://github.com/owen2345/pub_sub_model_sync. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## **License**\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## **Code of Conduct**\n\nEveryone interacting in the PubSubModelSync project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/pub_sub_model_sync/blob/master/CODE_OF_CONDUCT.md).\n\n## **Running tests**\n- `docker-compose run test`\n- `docker-compose run test bash -c \"rubocop\"`","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fowen2345%2Fpub_sub_model_sync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fowen2345%2Fpub_sub_model_sync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fowen2345%2Fpub_sub_model_sync/lists"}