Problem/Motivation
When you collect a large number of registrations (in the case of our website it's around 5000 registrants) and you manually add an instance to a Series, calculating the value of the computed field availability_count will cause your database server to go down.
The thing is:
If you go to a Series and use the "Add instance" option, after submitting the form you will get this:
SQLSTATE[HY000]: General error: 2006 MySQL server has gone away: INSERT INTO "cache_entity" ("cid", "expire", "created", "tags", "checksum", "data", "serialized") VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3....
This is because when making the POST request, the computation of the availability_count takes place, invoking the method Drupal\recurring_events_registration\Plugin\ComputedField\AvailabilityCount::computeValue().
That ends up using the Drupal\recurring_events_registration\RegistrationCreationService service to call retrieveAvailability(), which in turn calls retrieveRegisteredParties(TRUE, FALSE) that uses loadByProperties(). And here is where disaster comes:


Update: Along with the availability_count, there are a couple more computed fields that are also calculated when submitting the "Add instance" form: registration_count and waitlist_count, which, in turn, also call retrieveRegisteredParties(), generating more calls to loadByProperties() and making the process even heavier.
Steps to reproduce
- Have a large number of registrants, say 10000
- Go to a Series and use the "Add instance" option
- Try to save the instance
Proposed resolution
This gives me various considerations:
retrieveAvailability()must not rely onretrieveRegisteredParties()and a full load of the registrant entities just to get a count of the registrants. It should use another method based on Entity Query and thecount()method. Something like the following (not tested yet):
public function retrieveRegisteredPartiesCount($include_nonwaitlisted = TRUE, $include_waitlisted = TRUE, $uid = FALSE) { $query = $this->storage->getQuery(); if ($include_nonwaitlisted && !$include_waitlisted) { $query->condition('waitlist', 0); } elseif (!$include_nonwaitlisted && $include_waitlisted) { $query->condition('waitlist', 1); } if (!$include_waitlisted) { $query->condition('waitlist', 0); } if ($uid) { $query->condition('user_id', $uid); } switch ($this->getRegistrationType()) { case 'series': if (!empty($this->eventSeries->id())) { $query->condition('eventseries_id', $this->eventSeries->id()); } break; case 'instance': if (!empty($this->eventInstance->id())) { $query->condition('eventinstance_id', $this->eventInstance->id()); } break; } $result = $query->count()->execute(); return $result; }- Computed fields should not be calculated during the POST request when creating or editing an entity. I faced something similar in another project. The resulting values of computed fields are actually useful when getting/viewing/reading the entities, but in the POST request generated by a form submission and before the entity is saved, it does not make too much sense to me. I made an adjustment to various computed fields in another project: I verified the method of the request, and it case it is POST, I assigned an empty value to the computed field and do not perform the computation:
/** * {@inheritdoc} */ protected function computeValue() { // When saving or editing an entity, we are not interested in calculating // the values for its computed fields. The resulting values of these // computed fields are actually useful when getting/viewing/reading the // entities, for example, when retrieving an entity data from a GET request // to a REST or JSON:API endpoint. // If the request has the 'POST' method, assign an empty value to the // computed field and return. if ($this->requestStack->getCurrentRequest()->getMethod() == 'POST') { $this->list[0] = $this->createItem(0, NULL); return; } /* * The ComputedItemListTrait only calls this once on the same instance; from * then on, the value is automatically cached in $this->items, for use by * methods like getValue(). */ if (!isset($this->list[0])) { $entity = $this->getEntity(); $this->list[0] = $this->createItem(0, $this->getRegistrationCreationService($entity)->retrieveAvailability()); } } - Also, notice how the
$this->list[0]is defined just once. Read the comment aboutComputedItemListTraitonly calling this once
Remaining tasks
Check the changes proposed above and create a patch (done)
| Comment | File | Size | Author |
|---|---|---|---|
| #8 | 3391389-retrieveRegisteredPartiesCount-8.patch | 534 bytes | chrisla |
| #3 | recurring-events-3391389.patch | 13.19 KB | camilo.escobar |
Comments
Comment #2
camilo.escobar commentedComment #3
camilo.escobar commentedComment #4
camilo.escobar commentedComment #5
owenbush commentedComment #7
owenbush commentedThis looked good, thank you for your work and I thought the approach was really good.
Comment #8
chrisla commentedThe fix here is incompatible with Drupal 10 as it is missing the now required
$query->accessCheck()part of the query. A D10 site will break when the module is updated.Attached patch will fix for D10 users
Comment #9
christian deloach commentedThanks endless_wander. Patch works great. I confirm that it addresses the issue with the missing accessCheck.
Comment #10
chrisla commentedComment #12
owenbush commentedThe accesscheck patch has been committed to 2.0.x
Comment #13
camilo.escobar commentedThanks owenbush and endless_wander!