Skip to content

feat(integrations): contact cron for push and pull#4608

Merged
miguelpeixe merged 17 commits into
trunkfrom
feat/integrations-contact-cron
Apr 22, 2026
Merged

feat(integrations): contact cron for push and pull#4608
miguelpeixe merged 17 commits into
trunkfrom
feat/integrations-contact-cron

Conversation

@miguelpeixe
Copy link
Copy Markdown
Member

@miguelpeixe miguelpeixe commented Mar 30, 2026

All Submissions:

Changes proposed in this Pull Request:

Implements a contact cron system for push and pull to integrations, replacing the existing ActionScheduler strategy from Contact_Pull.

The cron runs every 5 minutes, processing a queue of recently active readers.

The push uses the existing sync strategy, which is backed by the retry strategy implemented with AS. The same strategy was added to the contact pull, so failed pulls are also handled.

To avoid piling up failed and duplicate requests, a new has_pending_retries( $user_id ) prevents the push or pull of unresolved requests.

How it works:

  1. On each logged-in page load, Contact_Cron::maybe_enqueue_contact() adds the user to both pull and push queues (throttled per-user to once every 5 minutes)
  2. If the user's data is stale (>24h), pull runs synchronously with a 1-second timeout to not hang the page load for too long. If it times out, it'll get picked up by the next step
  3. Every 5 minutes, WP-Cron fires Contact_Cron::handle_batch(), which processes both queues
  4. Failed pulls/pushes are retried via ActionScheduler with exponential backoff (30s → 2h, 5 attempts)

How to test the changes in this Pull Request:

  1. Make sure you have the NEWSPACK_LOG_LEVEL constant set to higher than 1
  2. Make sure you have some contact pull fields enabled for the ESP integration (Integrations/pull contact data #4496)
  3. Log in as a reader and verify the pull/push queues are populated:
wp user meta get <user_id> newspack_contact_cron_pull_pending
wp user meta get <user_id> newspack_contact_cron_push_pending
  1. Trigger the cron (wp cron event run newspack_contact_cron_batch) and verify that the user meta items are cleared, and the contact data is synced
  2. Mock stale data (>24h since last enqueue) to trigger synchronous pull on page load:
wp user meta update <user_id> newspack_contact_cron_last_enqueue $(( $(date +%s) - 86401 ))
  1. Refresh the page and confirm in the logs that the synchronous pull is called:

Synchronous pull started for user "<user_id>".

  1. Change your ESP to invalid credentials to simulate failed pushes and pulls
  2. Confirm the next cron run creates a newspack_contact_pull_retry AS action
  3. Restore the credentials and confirm the retry is resolved in the next run

Other information:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes, as applicable?
  • Have you successfully ran tests with your changes locally?

@miguelpeixe miguelpeixe marked this pull request as ready for review March 30, 2026 19:34
@miguelpeixe miguelpeixe requested a review from a team as a code owner March 30, 2026 19:34
@miguelpeixe miguelpeixe requested review from Copilot March 30, 2026 19:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements a WP-Cron–driven “contact cron” system to enqueue and batch-process reader contact pull (from integrations) and push (to integrations), replacing the prior Action Scheduler–driven orchestration used by Contact_Pull.

Changes:

  • Added Contact_Cron to enqueue active users and process pull/push queues every 5 minutes via WP-Cron.
  • Refactored Contact_Pull to focus on pulling + retry scheduling (Action Scheduler), with cron owning orchestration.
  • Updated unit tests to validate queue behavior instead of Action Scheduler async pull scheduling.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/unit-tests/integrations/class-test-integrations.php Updates tests to cover enqueue/queue + cron batch processing behavior.
includes/reader-activation/sync/class-contact-sync.php Adds a helper to detect pending sync retries for a user (used to avoid duplicate push work).
includes/reader-activation/integrations/class-contact-pull.php Refactors pull flow; adds Action Scheduler retry hook/backoff and supporting helpers.
includes/reader-activation/integrations/class-contact-cron.php New cron + queue orchestrator for recurring pull/push processing.
includes/reader-activation/class-integrations.php Loads/initializes the new cron orchestrator instead of the prior pull initializer.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread includes/reader-activation/sync/class-contact-sync.php
Comment thread includes/reader-activation/integrations/class-contact-pull.php
Comment thread includes/reader-activation/integrations/class-contact-pull.php
Comment thread includes/reader-activation/integrations/class-contact-cron.php Outdated
Comment thread includes/reader-activation/integrations/class-contact-cron.php
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread includes/reader-activation/integrations/class-contact-pull.php Outdated
Comment thread includes/reader-activation/integrations/class-contact-pull.php
Comment thread includes/reader-activation/integrations/class-contact-pull.php Outdated
Comment thread includes/reader-activation/integrations/class-contact-cron.php Outdated
Comment thread includes/reader-activation/integrations/class-contact-cron.php Outdated
Comment thread includes/reader-activation/integrations/class-contact-cron.php Outdated
Comment thread tests/unit-tests/integrations/class-test-integrations.php Outdated
- Fix has_pending_retries() to fetch all pending actions (per_page -1)
- Fix stale pull not enqueueing for batch on failure
- Add pull_sync() error return for cron fallback
- Update pull_sync docblock to match behavior
- Add deactivation hook and NEWSPACK_CRON_DISABLE support
- Only enqueue for pull when not stale; stale failures fall back to queue
@miguelpeixe miguelpeixe self-assigned this Mar 30, 2026
@miguelpeixe miguelpeixe requested a review from leogermani March 30, 2026 19:54
@miguelpeixe miguelpeixe added the [Status] Needs Review The issue or pull request needs to be reviewed label Mar 30, 2026
@leogermani
Copy link
Copy Markdown
Contributor

I have only read the code so far, and I have two questions... maybe easier if we get in a call but let's see:

1 - I don't see any control on the size of the queue. Isn't there a concern that the queue can get too big? Also, many parallel requests writing to the same huge option... And finally, when you start processing the queue, you clear it right away, what if that process times out before it gets to the end of the queue?

2 - Why do we need the backoff retry strategy? Aren't we retrying in 5 minutes anyways? This was a bit confusing to me

@miguelpeixe
Copy link
Copy Markdown
Member Author

miguelpeixe commented Apr 9, 2026

1 - I don't see any control on the size of the queue. Isn't there a concern that the queue can get too big? Also, many parallel requests writing to the same huge option...

If it's processed every 5 minutes, I don't expect it to get huge. We could rely entirely on object caching and skip the options table, but that would limit the functionality to the infrastructure, which is not ideal. Scratch that, we'd lose the queue on cache flushes.

And finally, when you start processing the queue, you clear it right away, what if that process times out before it gets to the end of the queue?

Good point. This actually relates to what we're talking about in p1774563416311379-slack-C07LB7B14GZ and I'm exploring in #4645. We should have a bulk method in the abstraction, defaulting to a loop of push_contact_data(), but allowing the integration to leverage its API.

Why do we need the backoff retry strategy? Aren't we retrying in 5 minutes anyways? This was a bit confusing to me

Not necessarily. If the user is not active in the next 5 minutes, we'll lose the sync. It's safer to rely on the retry and bail if there's a pending retry on the next cron run.

@miguelpeixe
Copy link
Copy Markdown
Member Author

Following our internal conversation, 36ed8cf refactors the queue strategy to use usermeta instead.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread includes/reader-activation/integrations/class-contact-cron.php
Comment thread includes/reader-activation/integrations/class-contact-cron.php
@github-actions github-actions Bot added [Status] Approved The pull request has been reviewed and is ready to merge and removed [Status] Needs Review The issue or pull request needs to be reviewed labels Apr 17, 2026
@miguelpeixe miguelpeixe merged commit 29e6668 into trunk Apr 22, 2026
9 checks passed
@miguelpeixe miguelpeixe deleted the feat/integrations-contact-cron branch April 22, 2026 14:37
@github-actions
Copy link
Copy Markdown

Hey @miguelpeixe, good job getting this PR merged! 🎉

Now, the needs-changelog label has been added to it.

Please check if this PR needs to be included in the "Upcoming Changes" and "Release Notes" doc. If it doesn't, simply remove the label.

If it does, please add an entry to our shared document, with screenshots and testing instructions if applicable, then remove the label.

Thank you! ❤️

matticbot pushed a commit that referenced this pull request May 7, 2026
# [6.40.0-alpha.1](v6.39.1...v6.40.0-alpha.1) (2026-05-07)

### Bug Fixes

* **access-control:** allow `0` member limit values to be shown ([#4670](#4670)) ([b7ea1e3](b7ea1e3))
* **access-control:** allow access rules with `supports_anonymous` to evaluate anonymous readers ([#4695](#4695)) ([3219d63](3219d63))
* **access-control:** bump content rule suggestion limit to 100 ([#4669](#4669)) ([64629eb](64629eb))
* **access-control:** ignore content rules with empty value ([#4675](#4675)) ([c01e97d](c01e97d))
* add `use` statements for clarity ([b0074c0](b0074c0))
* add 30s timeout to v2 invisible token acquisition to prevent hang ([8fd1a17](8fd1a17))
* add isolated flag to v2 invisible widget to prevent interference ([9cfcc30](9cfcc30))
* address potential race condition on multiple registrations with same email ([4fbb990](4fbb990))
* **advertising:** toggle spacing under category autocomplete ([1c65593](1c65593))
* **block-theme:** ensure Overlay Menu block panel opens in editor after template part switch ([#4642](#4642)) ([e45cfd0](e45cfd0))
* **campaigns:** remove wrapper div around add campaign modal input ([#4705](#4705)) ([e04d4af](e04d4af))
* centralize, normalize definition of `$referer` ([e38a94f](e38a94f))
* condition config output on RAS ([22c87af](22c87af))
* condition reCAPTCHA v3 actions on their `ready()` ([a70d5ff](a70d5ff))
* **content-gate:** pass redirectToLayout explicitly on Save ([#4702](#4702)) ([a95cd4d](a95cd4d))
* ensure idempotency by making sure callers get current reader data for logged-in readers ([be59918](be59918))
* gracefully reject calls if essential config is missing ([dd4c46b](dd4c46b))
* **indesign:** exclude rich media blocks from export output ([#4614](#4614)) ([6e45232](6e45232))
* make endpoint available only when RAS is enabled, per Copilot ([18de923](18de923))
* **memberships:** prevent duplicate teams on stripped-meta renewals ([#4661](#4661)) ([84f6c99](84f6c99))
* merge into existing `reader` now that we are out of POC ([3200cc0](3200cc0))
* move grecaptcha.execute inside try block to prevent Promise leak ([f224df8](f224df8))
* move reCAPTCHA behind rate limiting to protect metered service from floods ([0ec69a2](0ec69a2))
* **recaptcha:** skip script registration on TEC Community Events pages ([#4666](#4666)) ([d9a57c5](d9a57c5))
* reject Promise and provide helpful error if reCAPTCHA not happy ([099a109](099a109))
* send nonce, if available, with registration request ([ab57d81](ab57d81))
* textarea support ([8676e2d](8676e2d))
* use returned status rather than hardcoded value ([ea9af30](ea9af30))
* use server-side email, not submitted email, for logged-in users ([56e699f](56e699f))

### Features

* **access-control:** add Specific posts content rule ([#4674](#4674)) ([6735309](6735309))
* **access-control:** auto-signup on renewal only for already subscribed lists ([#4621](#4621)) ([23934c6](23934c6))
* **access-control:** human-readable group subscription names ([#4667](#4667)) ([a817304](a817304))
* **access-control:** tweaks to Access Control UI components ([#4659](#4659)) ([3c08943](3c08943))
* add block theme support to lite site ([#4628](#4628)) ([71632b9](71632b9))
* add overridable registration key methods to Integration base class ([2b4d04f](2b4d04f))
* add reCAPTCHA v2 invisible support to register() ([60b0e77](60b0e77))
* **block-theme:** add size options to the Overlay Menu ([#4652](#4652)) ([3b11b75](3b11b75))
* **block-theme:** move co-authors RSS feed code to the Newspack Plugin ([#4629](#4629)) ([50a160c](50a160c))
* **co-authors:** support CAP core entity store alongside legacy store ([#4673](#4673)) ([b80c49a](b80c49a))
* **components:** addToolbarBackButton util in newspack-components ([#4619](#4619)) ([ec36aa3](ec36aa3))
* **content-gate:** expose institutional IP allowlist endpoint ([#4685](#4685)) ([c6a054a](c6a054a))
* **data-events:** woo transactional events ([#4687](#4687)) ([544d718](544d718))
* delegate key generation and validation to Integration instances ([6aba746](6aba746))
* initial reader registration API rollup from working branch ([0250b2a](0250b2a))
* **integrations:** add activity logs view ([#4671](#4671)) ([fb98062](fb98062))
* **integrations:** add content gate metadata ([#4605](#4605)) ([dcd2a09](dcd2a09))
* **integrations:** contact cron for push and pull ([#4608](#4608)) ([29e6668](29e6668))
* **integrations:** redesign configure view ([#4668](#4668)) ([472dd06](472dd06))
* **integrations:** redesign dashboard with CardFeature grid ([#4665](#4665)) ([d20aae8](d20aae8))
* localize reCAPTCHA site key and version for both v2 and v3 ([56ba6ea](56ba6ea))
* more options for registration ([4c23648](4c23648))
* **newspack-ui:** refactor newsletter signup form with Newspack UI utilities ([#4491](#4491)) ([1859473](1859473))
* **reader-activation:** configure ESP incoming fields from schema ([#4676](#4676)) ([8bc84cc](8bc84cc))
* **reader-activation:** frontend registration API for integrations ([2d7a1bc](2d7a1bc))
* **settings:** Experimental Tools framework (NPPM-2692) ([#4591](#4591)) ([5a300eb](5a300eb))
* trigger a hook and invoke an integration callback when logged in user registers ([167383a](167383a))
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

🎉 This PR is included in version 6.40.0-alpha.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Status] Approved The pull request has been reviewed and is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants