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

ActionSchedule - Pass real batches into TokenProcessor. Simplify CRM_Activity_Tokens. #21088

Merged
merged 4 commits into from Aug 11, 2021

Conversation

totten
Copy link
Member

@totten totten commented Aug 11, 2021

Overview

When sending scheduled reminders, the method ActionSchedule::sendMailings() fetches a batch of pending reminders then composes+delivers a message for each. This PR changes the batch mechanics with an aim toward future simplifications in CRM_*_Tokens.

(Depends: #21085. Extracted from #21079.)

Before

For each item in the batch, it makes a new and separate instance of TokenProcessor. This means that the TokenProcessor only sees one pending reminder at a time. Thus, a method like CRM_Activity_Tokens::prefetch() can only prefetch one record at a time. In the context of scheduled reminders, prefetch() cannot meaningfully fetch batched data.

(This is why several variants of CRM_*_Tokens implement duplicate data-loads. eg One general-purpose data-load uses $context['activityId']+prefetch(); but an auxiliary data-load is needed for ActionSchedules. It uses $context['actionSearchResult']+alterActionScheduleQuery().)

After

It creates one TokenProcessor and adds multiple rows (one row for each pending reminder). This means a method like CRM_Activity_Tokens::prefetch() can see a full batch. In the context of scheduled reminders, TokenProcessor can meaningfully fetch batched data.

(This means that we don't need duplicate data-loads. The variants based on $context['actionSearchResult']+alterActionScheduleQuery() can become redundant.)

Comments

To demonstrate that the change works, the PR also updates CRM_Activity_Tokens to rely on the new mechanics and to pare-down actionSearchResult.

There is a fair amount of test-coverage for scheduled-reminders and tokens in CRM_Core_BAO_ActionScheduleTest and CRM_Activity_ActionMappingTest. The correctness of the PR mostly rests on the existing tests.

This try/catch block purports to catch any exceptions of type `TokenException`.

However, if you grep the source tree, you will find that `TokenException` is never thrown.
Background: `ActionSchedule::sendMailings()` fetches a batch of pending reminders (per
some specific schedule/rule). Then it composes and sends a message for each.

Before: For each item in the batch, it makes a new `TokenProcessor`.  This
means that the `TokenProcessor` only sees one pending reminder. Therefore, the
`TokenProcessor` cannot meaningfully fetch batched data.

After: It creates one `TokenProcessor` and adds rows for each pending
reminder.  This means that `TokenProcessor` can fetch batched data.# On
branch master-action-batch

Comments: This part of a longer plot to simplify the `CRM_*_Tokens`.
Currently, `CRM_*_Tokens` implements duplicate prefetch mechanisms -- the
general-purpose `prefetch()` and the special-purpose
`alterActionScheduleQuery()`.  This patch makes it possible to use
`prefetch()` for all the real work of prefetching (and phasing-out
`alterActionScheduleQuery()`).
This will help us to consolidate the prefetching logic in `CRM_*_Tokens`.

Currently, `CRM_*_Token::alterActionScheduleQuery()` updates the query to select all fields for the entity.
This is regardless of how many fields there are, whether they are needed, etc. This is also prone to naming conflicts.

With this patch, `CRM_*_Token::alterActionScheduleQuery()` only needs to select one field (`tokenContext_fooId`). This
will then propagate to the `TokenProcessor` which can do its own optimized data-loading.
…tionSearchResult`.

Before: There are two distinct ways in which `CRM_Activity_Tokens` can be called, eg

* For scheduled reminders, it's given `$row->context[actionSearchResult]`. The query is
  inspected for data.
* For everything else, it's given `$row->context[activityId]`. The value is

After: `CRM_Activity_Tokens` always receives activities as `$row->context[activityId]`.

Comment: There are pre-existing tests for activity-based reminders and tokens in
`CRM_Core_BAO_ActionScheduleTest` and `CRM_Activity_ActionMappingTest`.
@civibot
Copy link

civibot bot commented Aug 11, 2021

(Standard links)

}

foreach ($dao->toArray() as $key => $value) {
if (preg_match('/^tokenContext_(.*)/', $key, $m)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So really this is the meaningful change in here - I think some comments around it would help. The fact this still passes tests means it must still work for contribution tokens - but I'm quite sure how

Copy link
Member Author

Choose a reason for hiding this comment

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

@eileenmcnaughton Agree about comments. Added PR for that: #21098

For contribution tokens, this PR doesn't actually change anything. CRM_Contribution_Tokens::alterActionScheduleQuery() still does the same prefetch that it did before. That'll be the subsequent PR.

For this PR, I only touched CRM_Activity_Tokens::alterActionScheduleQuery() because that was the simplest one - a way to demonstrate the mechanism.

@eileenmcnaughton
Copy link
Contributor

@totten so in theory if we use this with the Event tokens (I don't know if they are tested) then in a multilingual database we would get (before and after) different titles and different event type values for 2 rows where the contact's preferred language is french in the first row & english in the second row ?

I'm confortable that we have tests on action schedule rendering at least one field in each of the entities that we handle in the scheduled reminders - but I haven't found any tests (yet) that check that we render per the contact's language

@eileenmcnaughton
Copy link
Contributor

I've realised that it would be possible to add multilingual testing to the contribution tokens test - we could test the db translation through the psuedoconstants & db-style if we extend it support contribution_status_id.thank_you_text - which is a format we've been discussing

@mattwire mattwire merged commit bb64ae5 into civicrm:master Aug 11, 2021
@mattwire
Copy link
Contributor

Thanks @totten this achieves what I wanted to get to in #16983

@eileenmcnaughton
Copy link
Contributor

@mattwire did you r-run the multilingual aspects before merging?

@mattwire
Copy link
Contributor

@eileenmcnaughton I didn't test the multilingual side because my understanding was that that is something we'd like to get working but has not been implemented until now?

@eileenmcnaughton
Copy link
Contributor

@mattwire I was assuming the references to locale in this PR & in the 'depends' pr https://github.com/civicrm/civicrm-core/pull/21085/files#diff-73feaf674c8b9ad9c02bb58a7bc1d05a2bf23a4cdc87ed281b51257ac773bf84R279-R281 meant something

@totten
Copy link
Member Author

totten commented Aug 11, 2021

so in theory if we use this with the Event tokens (I don't know if they are tested) then in a multilingual database we would get (before and after) different titles and different event type values for 2 rows where the contact's preferred language is french in the first row & english in the second row ?

Hmm, right, multilingual is more complicated than just {ts}. You have to fetch different columns from the DB.

We have two prefetching patterns on the table (prefetch() vs alterActionScheduleQuery()), and I don't think either will do what you're asking.

  • When prefetching via alterActionScheduleQuery(), it's one upfront query (ActionSchedule::prepareMailingQuery() => CRM_*_Tokens::alterActionScheduleQuery) that gets the data for all recipients. alterActionScheduleQuery() does not load subjectively based on the recipient-designated locale.
  • When prefetching via prefetch(), it's a couple upfront queries (TokenProcessor::evaluate()=>CRM_*_Tokens::prefetch()) that get the data for all recipients. Neither evaluate() nor prefetch() loads subjectively based on recipient-designated locale.

For that goal, I think this PR leaves us in basically the same place as before -- maybe slightly better. I don't think it sets us back. Prefetching multilingual-subjective data with alterActionScheduleQuery() seems more difficult than doing it in prefetch(). (At least prefetch() can use array_unique($proc->getContextValues('locale')).) Additionally, consolidating on one way to do this loading (rather than two ways) should make it easier to iterate/adjust.

But I think that requirement is a bit ucky regardless. You'd either have to skip prefetching (ie load all data separately for every recipient), or you need a bigger data-structure for the cache (where it tracks locales as well as entity-ids and field-names). So, for example, one might edit CRM_Event_Tokens::prefetch() to fetch the civicrm_event.title in all active locales. Or one might have an entity-cache that is locale-aware. Not simple, but more feasible than with alterActionScheduleQuery().

@eileenmcnaughton
Copy link
Contributor

OK - that makes sense for the db fetches. I would have either written a test or r-run with some location testing to test it before merging but since it's already merged I will let it be

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
3 participants