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

Add option to drop datastore if there is an error on post_import #4311

Merged
merged 8 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions cypress/integration/09_admin_links.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ context('Administration pages', () => {
const links = [
'Datasets',
'Datastore Import Status',
'Datastore settings',
'Datastore Settings',
'Data Dictionary',
'Harvests',
'Metastore settings',
'Resources'
'Metastore Settings',
'Resource Settings'
]

cy.visit(`${baseurl}/admin`)
Expand All @@ -25,19 +25,19 @@ context('Administration pages', () => {
})
})

it('Admin can access the Metastore settings.', () => {
it('Admin can access the Metastore Settings.', () => {
cy.get('.toolbar-icon-system-admin-dkan').contains('DKAN').next('.toolbar-menu').then($el=>{
cy.wrap($el).invoke('show')
cy.wrap($el).contains('Metastore settings')
cy.wrap($el).contains('Metastore Settings')
})
cy.visit(baseurl + "/admin/dkan/properties")
cy.get('.option').should('contain.text', 'Distribution (distribution)')
})

it('Admin can access the Datastore settings.', () => {
it('Admin can access the Datastore Settings.', () => {
cy.get('.toolbar-icon-system-admin-dkan').contains('DKAN').next('.toolbar-menu').then($el=>{
cy.wrap($el).invoke('show')
cy.wrap($el).contains('Datastore settings')
cy.wrap($el).contains('Datastore Settings')
})
cy.visit(baseurl + "/admin/dkan/datastore")
cy.get('label[for="edit-rows-limit"]').should('have.text', 'Rows limit')
Expand Down
2 changes: 2 additions & 0 deletions modules/datastore/config/schema/datastore.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ datastore.settings:
type: string
delete_local_resource:
type: boolean
drop_datastore_on_post_import_error:
type: boolean
4 changes: 2 additions & 2 deletions modules/datastore/datastore.links.menu.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
datastore.settings_form:
title: Datastore settings
title: Datastore Settings
description: Datastore settings.
parent: system.admin_dkan
route_name: datastore.settings
weight: 12

datastore.dkan_resource_settings:
title: Resources
title: Resource Settings
description: Resource options.
parent: system.admin_dkan
route_name: datastore.dkan_resource_settings
Expand Down
2 changes: 1 addition & 1 deletion modules/datastore/datastore.routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ datastore.sql_endpoint.post.api:
datastore.dkan_resource_settings:
path: '/admin/dkan/resources'
defaults:
_title: 'Resources'
_title: 'Resource Settings'
_form: 'Drupal\datastore\Form\ResourceSettingsForm'
requirements:
_permission: 'administer site configuration'
Expand Down
11 changes: 11 additions & 0 deletions modules/datastore/src/Form/ResourceSettingsForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'#default_value' => $this->config('datastore.settings')->get('delete_local_resource'),
'#description' => $this->t('Delete local copy of remote files after the datastore import is complete'),
];
$form['drop_datastore_on_post_import_error'] = [
'#type' => 'checkbox',
'#title' => $this->t('Drop the datastore table if the post import queue reports an error.'),
'#default_value' => $this->config('datastore.settings')->get('drop_datastore_on_post_import_error'),
'#description' => $this->t('The datastore import queue brings in all columns as strings. The post import
queue will alter the table according to the data dictionary, if there is a problem during this step the
error will be posted to the Datastore Import Status dashboard, and the datastore table will keep all
data typed as strings. Check this box if you prefer that the table be dropped if there is a problem
in the post import stage.'),
rovcase marked this conversation as resolved.
Show resolved Hide resolved
];
$form['resource_perspective_display'] = [
'#type' => 'select',
'#title' => $this->t('Resource download url display'),
Expand All @@ -76,6 +86,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
->set('purge_table', $form_state->getValue('purge_table'))
->set('purge_file', $form_state->getValue('purge_file'))
->set('delete_local_resource', $form_state->getValue('delete_local_resource'))
->set('drop_datastore_on_post_import_error', $form_state->getValue('drop_datastore_on_post_import_error'))
->save();
$this->config('metastore.settings')
->set('resource_perspective_display', $form_state->getValue('resource_perspective_display'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

namespace Drupal\datastore\Plugin\QueueWorker;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\common\DataResource;
use Drupal\datastore\DataDictionary\AlterTableQueryBuilderInterface;
use Drupal\datastore\PostImportResult;
use Drupal\datastore\DatastoreService;
use Drupal\datastore\Service\PostImport;
use Drupal\datastore\Service\ResourceProcessor\ResourceDoesNotHaveDictionary;
use Drupal\datastore\Service\ResourceProcessorCollector;
Expand All @@ -30,6 +32,13 @@
*/
class PostImportResourceProcessor extends QueueWorkerBase implements ContainerFactoryPluginInterface {

/**
* The datastore.settings config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;

/**
* A logger channel for this plugin.
*
Expand All @@ -51,6 +60,13 @@ class PostImportResourceProcessor extends QueueWorkerBase implements ContainerFa
*/
protected ResourceProcessorCollector $resourceProcessorCollector;

/**
* The datastore service.
*
* @var \Drupal\datastore\DatastoreService
*/
protected DatastoreService $datastoreService;

/**
* The PostImport service.
*
Expand Down Expand Up @@ -81,6 +97,8 @@ class PostImportResourceProcessor extends QueueWorkerBase implements ContainerFa
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* The config.factory service.
* @param \Drupal\datastore\DataDictionary\AlterTableQueryBuilderInterface $alter_table_query_builder
* The alter table query factory service.
* @param \Psr\Log\LoggerInterface $logger_channel
Expand All @@ -89,6 +107,8 @@ class PostImportResourceProcessor extends QueueWorkerBase implements ContainerFa
* The metastore resource mapper service.
* @param \Drupal\datastore\Service\ResourceProcessorCollector $processor_collector
* The resource processor collector service.
* @param \Drupal\datastore\DatastoreService $datastoreService
* The resource datastore service.
* @param \Drupal\datastore\Service\PostImport $post_import
* The post import service.
* @param \Drupal\metastore\DataDictionary\DataDictionaryDiscoveryInterface $data_dictionary_discovery
Expand All @@ -100,18 +120,22 @@ public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
ConfigFactoryInterface $configFactory,
AlterTableQueryBuilderInterface $alter_table_query_builder,
LoggerInterface $logger_channel,
ResourceMapper $resource_mapper,
ResourceProcessorCollector $processor_collector,
DatastoreService $datastoreService,
PostImport $post_import,
DataDictionaryDiscoveryInterface $data_dictionary_discovery,
ReferenceLookup $referenceLookup
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->config = $configFactory;
$this->logger = $logger_channel;
$this->resourceMapper = $resource_mapper;
$this->resourceProcessorCollector = $processor_collector;
$this->datastoreService = $datastoreService;
$this->postImport = $post_import;
$this->dataDictionaryDiscovery = $data_dictionary_discovery;
// Set the timeout for database connections to the queue lease time.
Expand All @@ -130,10 +154,12 @@ public static function create(ContainerInterface $container, array $configuratio
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory'),
$container->get('dkan.datastore.data_dictionary.alter_table_query_builder.mysql'),
$container->get('dkan.datastore.logger_channel'),
$container->get('dkan.metastore.resource_mapper'),
$container->get('dkan.datastore.service.resource_processor_collector'),
$container->get('dkan.datastore.service'),
$container->get('dkan.datastore.service.post_import'),
$container->get('dkan.metastore.data_dictionary_discovery'),
$container->get('dkan.metastore.reference_lookup'),
Expand All @@ -145,13 +171,28 @@ public static function create(ContainerInterface $container, array $configuratio
*/
public function processItem($data) {
$postImportResult = $this->postImportProcessItem($data);
$drop_config = $this->config->get('datastore.settings')
->get('drop_datastore_on_post_import_error');

if ($postImportResult->getPostImportStatus() === 'done') {
$this->invalidateCacheTags(DataResource::buildUniqueIdentifier(
$data->getIdentifier(),
$data->getVersion(),
DataResource::DEFAULT_SOURCE_PERSPECTIVE
));
}
if ($postImportResult->getPostImportStatus() === 'error' && $drop_config) {
$identifier = $data->getIdentifier();
try {
$this->datastoreService->drop($identifier, NULL, FALSE);
$this->logger->notice('Successfully dropped the datastore for resource @identifier due to a post import error. Visit the Datastore Import Status dashboard for details.', [
'@identifier' => $identifier,
]);
}
catch (\Exception $e) {
$this->logger->error($e->getMessage());
}
}
// Store the results of the PostImportResult object.
$postImportResult->storeResult();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<?php

declare(strict_types=1);

namespace Drupal\Tests\datastore\Kernel\Plugin\QueueWorker;

use Drupal\common\DataResource;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\datastore\DatastoreService;
use Drupal\datastore\Plugin\QueueWorker\PostImportResourceProcessor;
use Drupal\datastore\PostImportResult;
use Drupal\datastore\Service\ResourceProcessor\DictionaryEnforcer;
use Drupal\datastore\Service\ResourceProcessor\ResourceDoesNotHaveDictionary;
use Drupal\KernelTests\KernelTestBase;
Expand Down Expand Up @@ -90,4 +95,162 @@ public function testPostImportProcessItemNoDictionary() {
);
}

/**
* @covers ::processItem
*/
public function testProcessItem() {
$data_identifier = 'test_identifier';

$this->config('datastore.settings')
->set('drop_datastore_on_post_import_error', TRUE)
->save();

// Our error result.
$error_result = $this->getMockBuilder(PostImportResult::class)
->disableOriginalConstructor()
->onlyMethods(['getPostImportStatus', 'storeResult'])
->getMock();
$error_result->expects($this->any())
->method('getPostImportStatus')
->willReturn('error');
$error_result->expects($this->once())
->method('storeResult');

// Mock a logger to expect error logging.
$logger = $this->getMockBuilder(LoggerChannelInterface::class)
->onlyMethods(['error', 'notice'])
->getMockForAbstractClass();
// Never expect an error.
$logger->expects($this->never())
->method('error');
// Expect one notice.
$logger->expects($this->once())
->method('notice')
->with(
'Successfully dropped the datastore for resource @identifier due to a post import error. Visit the Datastore Import Status dashboard for details.',
['@identifier' => $data_identifier],
);
$this->container->set('dkan.datastore.logger_channel', $logger);

// Datastore service will always succeed. Mocked so we don't have to deal
// with dropping an actual datastore.
$datastore_service = $this->getMockBuilder(DatastoreService::class)
->disableOriginalConstructor()
->onlyMethods(['drop'])
->getMock();
$datastore_service->expects($this->once())
->method('drop');
// Put the service into the service container.
$this->container->set('dkan.datastore.service', $datastore_service);

// Return our error result.
$post_import_resource_processor = $this->getMockBuilder(PostImportResourceProcessor::class)
->setConstructorArgs([
[],
'',
['cron' => ['lease_time' => 10800]],
$this->container->get('config.factory'),
$this->container->get('dkan.datastore.data_dictionary.alter_table_query_builder.mysql'),
$this->container->get('dkan.datastore.logger_channel'),
$this->container->get('dkan.metastore.resource_mapper'),
$this->container->get('dkan.datastore.service.resource_processor_collector'),
$this->container->get('dkan.datastore.service'),
$this->container->get('dkan.datastore.service.post_import'),
$this->container->get('dkan.metastore.data_dictionary_discovery'),
$this->container->get('dkan.metastore.reference_lookup'),
])
->onlyMethods(['postImportProcessItem'])
->getMock();
$post_import_resource_processor->expects($this->once())
->method('postImportProcessItem')
->willReturn($error_result);

// Data we'll pass to our method under test.
$data = $this->getMockBuilder(DataResource::class)
->disableOriginalConstructor()
->onlyMethods(['getIdentifier'])
->getMock();
$data->expects($this->once())
->method('getIdentifier')
->willReturn($data_identifier);

$post_import_resource_processor->processItem($data);
}

/**
* @covers ::processItem
*/
public function testProcessItemExceptionPath() {
$this->config('datastore.settings')
->set('drop_datastore_on_post_import_error', TRUE)
->save();

// Our error result.
$error_result = $this->getMockBuilder(PostImportResult::class)
->disableOriginalConstructor()
->onlyMethods(['getPostImportStatus', 'storeResult'])
->getMock();
$error_result->expects($this->any())
->method('getPostImportStatus')
->willReturn('error');
$error_result->expects($this->once())
->method('storeResult');

// Mock a logger to expect error logging.
$logger = $this->getMockBuilder(LoggerChannelInterface::class)
->onlyMethods(['error', 'notice'])
->getMockForAbstractClass();
// Expect an error.
$logger->expects($this->once())
->method('error');
// Expect no notices.
$logger->expects($this->never())
->method('notice');
$this->container->set('dkan.datastore.logger_channel', $logger);

// Datastore service rigged to explode.
$datastore_service = $this->getMockBuilder(DatastoreService::class)
->disableOriginalConstructor()
->onlyMethods(['drop'])
->getMock();
$datastore_service->expects($this->once())
->method('drop')
->willThrowException(new \Exception('our test message'));
// Put the service into the service container.
$this->container->set('dkan.datastore.service', $datastore_service);

// Return our error result.
$post_import_resource_processor = $this->getMockBuilder(PostImportResourceProcessor::class)
->setConstructorArgs([
[],
'',
['cron' => ['lease_time' => 10800]],
$this->container->get('config.factory'),
$this->container->get('dkan.datastore.data_dictionary.alter_table_query_builder.mysql'),
$this->container->get('dkan.datastore.logger_channel'),
$this->container->get('dkan.metastore.resource_mapper'),
$this->container->get('dkan.datastore.service.resource_processor_collector'),
$this->container->get('dkan.datastore.service'),
$this->container->get('dkan.datastore.service.post_import'),
$this->container->get('dkan.metastore.data_dictionary_discovery'),
$this->container->get('dkan.metastore.reference_lookup'),
])
->onlyMethods(['postImportProcessItem'])
->getMock();
$post_import_resource_processor->expects($this->once())
->method('postImportProcessItem')
->willReturn($error_result);

// Data we'll pass to our method under test.
$data = $this->getMockBuilder(DataResource::class)
->disableOriginalConstructor()
->onlyMethods(['getIdentifier'])
->getMock();
$data->expects($this->once())
->method('getIdentifier')
->willReturn('test');

$post_import_resource_processor->processItem($data);
}

}
Loading