![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/mautic.corals.io/app/bundles/CampaignBundle/Executioner/Scheduler/ |
<?php namespace Mautic\CampaignBundle\Executioner\Scheduler; use Doctrine\Common\Collections\ArrayCollection; use Mautic\CampaignBundle\CampaignEvents; use Mautic\CampaignBundle\Entity\Event; use Mautic\CampaignBundle\Entity\LeadEventLog; use Mautic\CampaignBundle\Event\ScheduledBatchEvent; use Mautic\CampaignBundle\Event\ScheduledEvent; use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor; use Mautic\CampaignBundle\EventCollector\EventCollector; use Mautic\CampaignBundle\Executioner\Exception\IntervalNotConfiguredException; use Mautic\CampaignBundle\Executioner\Logger\EventLogger; use Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException; use Mautic\CampaignBundle\Executioner\Scheduler\Mode\DateTime; use Mautic\CampaignBundle\Executioner\Scheduler\Mode\Interval; use Mautic\CoreBundle\Helper\CoreParametersHelper; use Mautic\LeadBundle\Entity\Lead; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class EventScheduler { public function __construct( private LoggerInterface $logger, private EventLogger $eventLogger, private Interval $intervalScheduler, private DateTime $dateTimeScheduler, private EventCollector $collector, private EventDispatcherInterface $dispatcher, private CoreParametersHelper $coreParametersHelper ) { } public function scheduleForContact(Event $event, \DateTimeInterface $executionDate, Lead $contact): void { $contacts = new ArrayCollection([$contact]); $this->schedule($event, $executionDate, $contacts); } /** * @param bool $isInactiveEvent */ public function schedule(Event $event, \DateTimeInterface $executionDate, ArrayCollection $contacts, $isInactiveEvent = false): void { $config = $this->collector->getEventConfig($event); // Load the rotations for creating new log entries $this->eventLogger->hydrateContactRotationsForNewLogs($contacts->getKeys(), $event->getCampaign()->getId()); // If this is relative to a specific hour, process the contacts in batches by contacts' timezone if ($this->intervalScheduler->isContactSpecificExecutionDateRequired($event)) { $groupedExecutionDates = $this->intervalScheduler->groupContactsByDate($event, $contacts, $executionDate); foreach ($groupedExecutionDates as $groupExecutionDateDAO) { $this->scheduleEventForContacts( $event, $config, $groupExecutionDateDAO->getExecutionDate(), $groupExecutionDateDAO->getContacts(), $isInactiveEvent ); } return; } // Otherwise just schedule as the default $this->scheduleEventForContacts($event, $config, $executionDate, $contacts, $isInactiveEvent); } public function reschedule(LeadEventLog $log, \DateTimeInterface $toBeExecutedOn): void { $log->setTriggerDate($toBeExecutedOn); $this->eventLogger->persistLog($log); $event = $log->getEvent(); $config = $this->collector->getEventConfig($event); $this->dispatchScheduledEvent($config, $log, true); } /** * @param ArrayCollection|LeadEventLog[] $logs */ public function rescheduleLogs(ArrayCollection $logs, \DateTimeInterface $toBeExecutedOn): void { foreach ($logs as $log) { $log->setTriggerDate($toBeExecutedOn); } $this->eventLogger->persistCollection($logs); $event = $logs->first()->getEvent(); $config = $this->collector->getEventConfig($event); $this->dispatchBatchScheduledEvent($config, $event, $logs, true); } /** * @deprecated since Mautic 3. To be removed in Mautic 4. Use rescheduleFailures instead. */ public function rescheduleFailure(LeadEventLog $log): void { try { $this->reschedule($log, $this->getRescheduleDate($log)); } catch (IntervalNotConfiguredException) { // Do not reschedule if an interval was not configured. } } public function rescheduleFailures(ArrayCollection $logs): void { if (!$logs->count()) { return; } foreach ($logs as $log) { try { $this->reschedule($log, $this->getRescheduleDate($log)); } catch (IntervalNotConfiguredException) { // Do not reschedule if an interval was not configured. } } // Send out a batch event $event = $logs->first()->getEvent(); $config = $this->collector->getEventConfig($event); $this->dispatchBatchScheduledEvent($config, $event, $logs, true); } /** * @throws NotSchedulableException */ public function getExecutionDateTime(Event $event, \DateTimeInterface $compareFromDateTime = null, \DateTime $comparedToDateTime = null): \DateTimeInterface { if (null === $compareFromDateTime) { $compareFromDateTime = new \DateTime(); } else { // Prevent comparisons from modifying original object $compareFromDateTime = clone $compareFromDateTime; } if (null === $comparedToDateTime) { $comparedToDateTime = clone $compareFromDateTime; } else { // Prevent comparisons from modifying original object $comparedToDateTime = clone $comparedToDateTime; } switch ($event->getTriggerMode()) { case Event::TRIGGER_MODE_IMMEDIATE: case null: // decision $this->logger->debug('CAMPAIGN: ('.$event->getId().') Executing immediately'); return $compareFromDateTime; case Event::TRIGGER_MODE_INTERVAL: return $this->intervalScheduler->getExecutionDateTime($event, $compareFromDateTime, $comparedToDateTime); case Event::TRIGGER_MODE_DATE: return $this->dateTimeScheduler->getExecutionDateTime($event, $compareFromDateTime, $comparedToDateTime); } throw new NotSchedulableException(); } /** * @return \DateTimeInterface * * @throws NotSchedulableException */ public function validateExecutionDateTime(LeadEventLog $log, \DateTime $currentDateTime) { if (!$scheduledDateTime = $log->getTriggerDate()) { throw new NotSchedulableException(); } $event = $log->getEvent(); switch ($event->getTriggerMode()) { case Event::TRIGGER_MODE_IMMEDIATE: case null: // decision $this->logger->debug('CAMPAIGN: ('.$event->getId().') Executing immediately'); return $currentDateTime; case Event::TRIGGER_MODE_INTERVAL: return $this->intervalScheduler->validateExecutionDateTime($log, $currentDateTime); case Event::TRIGGER_MODE_DATE: return $this->dateTimeScheduler->getExecutionDateTime($event, $currentDateTime, $scheduledDateTime); } throw new NotSchedulableException(); } /** * @param ArrayCollection|Event[] $events * * @throws NotSchedulableException */ public function getSortedExecutionDates(ArrayCollection $events, \DateTimeInterface $lastActiveDate): array { $eventExecutionDates = []; /** @var Event $child */ foreach ($events as $child) { $eventExecutionDates[$child->getId()] = $this->getExecutionDateTime($child, $lastActiveDate); } uasort( $eventExecutionDates, fn (\DateTimeInterface $a, \DateTimeInterface $b): int => $a <=> $b ); return $eventExecutionDates; } public function getExecutionDateForInactivity(\DateTimeInterface $eventExecutionDate, \DateTimeInterface $earliestExecutionDate, \DateTimeInterface $now): \DateTimeInterface { if ($eventExecutionDate->getTimestamp() === $earliestExecutionDate->getTimestamp()) { // Inactivity is based on the "wait" period so execute now return clone $now; } return $eventExecutionDate; } public function shouldSchedule(\DateTimeInterface $executionDate, \DateTimeInterface $now): bool { // Mainly for functional tests so we don't have to wait minutes but technically can be used in an environment as well if this behavior // is desired by system admin if (false === (bool) getenv('CAMPAIGN_EXECUTIONER_SCHEDULER_ACKNOWLEDGE_SECONDS')) { // Purposively ignore seconds to prevent rescheduling based on a variance of a few seconds $executionDate = new \DateTime($executionDate->format('Y-m-d H:i'), $executionDate->getTimezone()); $now = new \DateTime($now->format('Y-m-d H:i'), $now->getTimezone()); } return $executionDate > $now; } public function shouldScheduleEvent(Event $event, \DateTimeInterface $executionDate, \DateTimeInterface $now): bool { if ($this->intervalScheduler->isContactSpecificExecutionDateRequired($event)) { // Event has days in week specified. Needs to be recalculated to the next day configured return true; } return $this->shouldSchedule($executionDate, $now); } /** * @throws NotSchedulableException */ public function validateAndScheduleEventForContacts(Event $event, \DateTimeInterface $executionDateTime, ArrayCollection $contacts, \DateTimeInterface $comparedFromDateTime): void { if ($this->intervalScheduler->isContactSpecificExecutionDateRequired($event)) { $this->logger->debug( 'CAMPAIGN: Event ID# '.$event->getId(). ' has to be scheduled based on contact specific parameters '. ' compared to '.$executionDateTime->format('Y-m-d H:i:s') ); $groupedExecutionDates = $this->intervalScheduler->groupContactsByDate($event, $contacts, $executionDateTime); $config = $this->collector->getEventConfig($event); foreach ($groupedExecutionDates as $groupExecutionDateDAO) { $this->scheduleEventForContacts( $event, $config, $groupExecutionDateDAO->getExecutionDate(), $groupExecutionDateDAO->getContacts() ); } return; } if ($this->shouldSchedule($executionDateTime, $comparedFromDateTime)) { $this->schedule($event, $executionDateTime, $contacts); return; } throw new NotSchedulableException(); } /** * @param bool $isReschedule */ private function dispatchScheduledEvent(AbstractEventAccessor $config, LeadEventLog $log, $isReschedule = false): void { $this->dispatcher->dispatch( new ScheduledEvent($config, $log, $isReschedule), CampaignEvents::ON_EVENT_SCHEDULED ); } /** * @param bool $isReschedule */ private function dispatchBatchScheduledEvent(AbstractEventAccessor $config, Event $event, ArrayCollection $logs, $isReschedule = false): void { if (!$logs->count()) { return; } $this->dispatcher->dispatch( new ScheduledBatchEvent($config, $event, $logs, $isReschedule), CampaignEvents::ON_EVENT_SCHEDULED_BATCH ); } /** * @param bool $isInactiveEvent */ private function scheduleEventForContacts(Event $event, AbstractEventAccessor $config, \DateTimeInterface $executionDate, ArrayCollection $contacts, $isInactiveEvent = false): void { foreach ($contacts as $contact) { // Create the entry $log = $this->eventLogger->buildLogEntry($event, $contact, $isInactiveEvent); // Schedule it $log->setTriggerDate($executionDate); // Add it to the queue to persist to the DB $this->eventLogger->queueToPersist($log); // lead actively triggered this event, a decision wasn't involved, or it was system triggered and a "no" path so schedule the event to be fired at the defined time $this->logger->debug( 'CAMPAIGN: '.ucfirst($event->getEventType()).' ID# '.$event->getId().' for contact ID# '.$contact->getId() .' has timing that is not appropriate and thus scheduled for '.$executionDate->format('Y-m-d H:i:s T') ); $this->dispatchScheduledEvent($config, $log); } // Persist any pending in the queue $logs = $this->eventLogger->persistQueuedLogs(); // Send out a batch event $this->dispatchBatchScheduledEvent($config, $event, $logs); // Update log entries and clear from memory $this->eventLogger->persistCollection($logs) ->clearCollection($logs); } /** * @throws IntervalNotConfiguredException */ private function getRescheduleDate(LeadEventLog $leadEventLog): \DateTimeInterface { $rescheduleDate = new \DateTime(); $logInterval = $leadEventLog->getRescheduleInterval(); if ($logInterval) { return $rescheduleDate->add($logInterval); } $defaultIntervalString = $this->coreParametersHelper->get('campaign_time_wait_on_event_false'); if (!$defaultIntervalString) { throw new IntervalNotConfiguredException('No Interval has been set on the lead event log nor as campaign_time_wait_on_event_false config value.'); } try { return $rescheduleDate->add(new \DateInterval($defaultIntervalString)); } catch (\Exception) { // Bad interval throw new IntervalNotConfiguredException("'{$defaultIntervalString}' is not valid interval string for campaign_time_wait_on_event_false config key."); } } }