Skip to content
Snippets Groups Projects
Commit 1619679e authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3404631: Integrity constraint violation during INSERT INTO vgwort_entity_registration

parent 86512263
No related branches found
Tags 2.0.0-beta9
1 merge request!34Test with schedular and content_moderation
Pipeline #59874 passed with warnings
......@@ -9,7 +9,8 @@
},
"require-dev": {
"drush/drush": "^11.0",
"drupal/graphql": "^4.4"
"drupal/graphql": "^4.4",
"drupal/scheduler_content_moderation_integration": "^2.0@beta"
},
"extra": {
"drush": {
......
......@@ -8,11 +8,6 @@ parameters:
count: 3
path: tests/src/Functional/DrushTest.php
-
message: "#^Access to undefined constant Drupal\\\\Tests\\\\vgwort\\\\Kernel\\\\DataProducer\\\\VgWortDataProducerTest\\:\\:ENTITY_TYPE\\.$#"
count: 1
path: tests/src/Kernel/DataProducer/VgWortDataProducerTest.php
-
message: """
#^Call to deprecated method expectError\\(\\) of class PHPUnit\\\\Framework\\\\TestCase\\:
......
......@@ -10,6 +10,7 @@ use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\vgwort\Plugin\AdvancedQueue\JobType\RegistrationNotification;
/**
......@@ -39,6 +40,13 @@ final class EntityJobMapper {
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The lock service.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected LockBackendInterface $lock;
/**
* Constructs a EntityJobMapper object.
*
......@@ -46,10 +54,13 @@ final class EntityJobMapper {
* The database connection to sort VG Wort entity registrations.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock service.
*/
public function __construct(Connection $connection, EntityTypeManagerInterface $entityTypeManager) {
public function __construct(Connection $connection, EntityTypeManagerInterface $entityTypeManager, LockBackendInterface $lock) {
$this->connection = $connection;
$this->entityTypeManager = $entityTypeManager;
$this->lock = $lock;
}
/**
......@@ -109,6 +120,22 @@ final class EntityJobMapper {
public function addJobToMap(Job $job, string $counter_id) {
[$entity_type, $entity_id] = RegistrationNotification::getEntityInfoFromJob($job);
// Since we're doing a select, delete and then insert we need to be sure
// that no other process is doing the same thing at the same time.
$lock_name = "EntityJobMapper::addJobToMap:$entity_type:$entity_id:$counter_id";
if (!$this->lock->acquire($lock_name)) {
$this->lock->wait($lock_name);
if (!$this->lock->acquire($lock_name)) {
// We should never get here. Even if we do we can return it is not that
// big a problem if multiple items end up in the queue for the same
// entity. The reason we maintain the map is to keep duplicates to a
// minimum. EntityJobMapper::markSuccessful() will create the missing
// row if required.
// @see \Drupal\vgwort\EntityJobMapper::markSuccessful()
return $this;
}
}
// Use a transaction to keep everything consistent. It will be committed as
// soon as $transaction is out of scope.
$transaction = $this->connection->startTransaction();
......@@ -161,10 +188,12 @@ final class EntityJobMapper {
])
->execute();
if ($transaction) {
// Commit the transaction.
// Commit the transaction.
if (isset($transaction)) {
$transaction = NULL;
}
$this->lock->release($lock_name);
return $this;
}
......
......@@ -51,3 +51,22 @@ function vgwort_test_vgwort_entity_counter_id_field(EntityInterface $entity): ?s
assert($entity instanceof FieldableEntityInterface, 'The entity is fieldable');
return \Drupal::state()->get('vgwort_test_vgwort_entity_counter_id_field');
}
/**
* Implements hook_entity_insert().
*/
function vgwort_test_entity_insert(EntityInterface $entity) {
vgwort_test_entity_update($entity);
}
/**
* Implements hook_entity_update().
*/
function vgwort_test_entity_update(EntityInterface $entity) {
static $recursion = FALSE;
if (\Drupal::state()->get('vgwort_test_entity_save_in_entity_save') && !$recursion) {
$recursion = TRUE;
$entity->save();
$recursion = FALSE;
}
}
......@@ -491,6 +491,34 @@ class EntityQueueTest extends VgWortKernelTestBase {
$this->assertSame('2', $job->getNumRetries());
}
/**
* Tests VGWort queueing and entity mapper with a recursive entity save.
*/
public function testEntitySaveInSave(): void {
$this->container->get('state')->set('vgwort_test_entity_save_in_entity_save', TRUE);
$user = User::create(['name' => 'test', 'status' => TRUE]);
$user->save();
$entity_storage = $this->container->get('entity_type.manager')
->getStorage(static::ENTITY_TYPE);
/** @var \Drupal\entity_test\Entity\EntityTestRevPub $entity */
$entity = $entity_storage->create([
'text' => 'Some text',
'name' => 'A title',
'user_id' => $user->id(),
]);
$entity->save();
$jobs = self::JOB_COUNT;
$jobs[Job::STATE_QUEUED] = '1';
/** @var \Drupal\advancedqueue\Entity\Queue $queue */
$queue = Queue::load('vgwort');
/** @var \Drupal\advancedqueue\Plugin\AdvancedQueue\Backend\Database $queue_backend */
$queue_backend = $queue->getBackend();
$this->container->get('advancedqueue.processor')->processQueue($queue);
$this->assertSame($jobs, $queue_backend->countJobs());
}
/**
* Determines if an entity is in the map table.
*
......
<?php
namespace Drupal\Tests\vgwort\Kernel;
use Drupal\advancedqueue\Entity\Queue;
use Drupal\advancedqueue\Job;
use Drupal\Tests\scheduler_content_moderation_integration\Kernel\SchedulerContentModerationTestBase;
use Drupal\Tests\vgwort\Traits\KernelSetupTrait;
/**
* Tests the vgwort entity queue with scheduler and content_moderation..
*
* @group vgwort
*/
class SchedulerIntegrationTest extends SchedulerContentModerationTestBase {
use KernelSetupTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['advancedqueue', 'vgwort'];
private const JOB_COUNT = [
Job::STATE_QUEUED => 0,
Job::STATE_PROCESSING => 0,
Job::STATE_SUCCESS => 0,
Job::STATE_FAILURE => 0,
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installVgWort();
}
/**
* Tests moderated entity publish scheduling with VGWort queueing.
*/
public function testIntegration(): void {
$queue = Queue::load('vgwort');
$this->assertInstanceOf(Queue::class, $queue);
$storage = \Drupal::service('entity_type.manager')->getStorage('node');
$entity = $this->createEntity('node', 'example', [
'title' => 'Published title',
'moderation_state' => 'draft',
'publish_on' => strtotime('yesterday'),
'publish_state' => 'published',
]);
$entity_id = $entity->id();
// Make sure entity is unpublished.
$this->assertFalse($entity->isPublished());
// Make sure the entity is not queued.
$this->assertSame(static::JOB_COUNT, $queue->getBackend()->countJobs());
$this->container->get('cron')->run();
$entity = $storage->loadRevision($storage->getLatestRevisionId($entity_id));
// Assert entity is now published.
$this->assertTrue($entity->isPublished());
$this->assertEquals('published', $entity->moderation_state->value);
// Assert entity is now queued.
$jobs = static::JOB_COUNT;
$jobs[Job::STATE_QUEUED] = '1';
$this->assertSame($jobs, $queue->getBackend()->countJobs());
$job = $this->container->get('vgwort.entity_job_mapper')->getJob($entity);
$this->assertSame(Job::STATE_QUEUED, $job->getState());
$this->assertSame('1', $job->getId());
}
}
......@@ -44,6 +44,7 @@ trait KernelSetupTrait {
}
if (array_key_exists('ENTITY_TYPE', (new \ReflectionClass($this))->getConstants())) {
// @phpstan-ignore-next-line
_vgwort_add_entity_reference_to_participant_map($this::ENTITY_TYPE, 'user_id');
}
}
......
......@@ -15,4 +15,4 @@ services:
arguments: ['@config.factory', '@vgwort.entity_job_mapper']
vgwort.entity_job_mapper:
class: Drupal\vgwort\EntityJobMapper
arguments: ['@database', '@entity_type.manager']
arguments: ['@database', '@entity_type.manager', '@lock']
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment