# --
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --
## nofilter(TidyAll::Plugin::OTRS::Perl::NoExitInConsoleCommands)
package Kernel::System::Console::Command::Maint::Ticket::FulltextIndexRebuildWorker;
use strict;
use warnings;
use parent qw(Kernel::System::Console::BaseCommand);
use POSIX ":sys_wait_h";
use Time::HiRes qw(sleep);
use Kernel::System::VariableCheck qw(:all);
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::DB',
'Kernel::System::Log',
'Kernel::System::PID',
'Kernel::System::Ticket',
'Kernel::System::Ticket::Article',
);
sub Configure {
my ( $Self, %Param ) = @_;
$Self->Description('Rebuild the article search index for needed articles.');
$Self->AddOption(
Name => 'children',
Description => "Specify the number of child processes to be used for indexing (default: 4, maximum: 20).",
Required => 0,
HasValue => 1,
ValueRegex => qr/^\d+$/smx,
);
$Self->AddOption(
Name => 'limit',
Description => "Maximum number of ArticleIDs to process (default: 20000).",
Required => 0,
HasValue => 1,
ValueRegex => qr/^\d+$/smx,
);
$Self->AddOption(
Name => 'force-pid',
Description => "Start even if another process is still registered in the database.",
Required => 0,
HasValue => 0,
);
return;
}
sub PreRun {
my ( $Self, %Param ) = @_;
my $Children = $Self->GetOption('children') // 4;
if ( $Children > 20 ) {
die "The allowed maximum amount of child processes is 20!\n";
}
my $ForcePID = $Self->GetOption('force-pid');
my $PIDObject = $Kernel::OM->Get('Kernel::System::PID');
my %PID = $PIDObject->PIDGet(
Name => 'ArticleSearchIndexRebuild',
);
if ( %PID && !$ForcePID ) {
die "Active indexing process already running! Skipping...\n";
}
my $Success = $PIDObject->PIDCreate(
Name => 'ArticleSearchIndexRebuild',
Force => $Self->GetOption('force-pid'),
);
if ( !$Success ) {
die "Unable to register indexing process! Skipping...\n";
}
return;
}
sub Run {
my ( $Self, %Param ) = @_;
my $Children = $Self->GetOption('children') // 4;
my $Limit = $Self->GetOption('limit') // 20000;
my %ArticleTicketIDs = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleSearchIndexRebuildFlagList(
Value => 1,
Limit => $Limit,
);
# perform the indexing if needed
if (%ArticleTicketIDs) {
$Self->ArticleIndexRebuild(
ArticleTicketIDs => \%ArticleTicketIDs,
Children => $Children,
);
}
else {
$Self->Print("No indexing needed! Skipping...\n");
}
$Self->Print("Done.\n");
return $Self->ExitCodeOk();
}
sub ArticleIndexRebuild {
my ( $Self, %Param ) = @_;
my @ArticleIDs = keys %{ $Param{ArticleTicketIDs} };
$Kernel::OM->Get('Kernel::System::DB')->Disconnect();
# Destroy objects for the child processes.
$Kernel::OM->ObjectsDiscard(
Objects => [
'Kernel::System::DB',
],
ForcePackageReload => 0,
);
# Split ArticleIDs into equal arrays for the child processes.
my @ArticleChunks;
my $Count = 0;
for my $ArticleID (@ArticleIDs) {
push @{ $ArticleChunks[ $Count++ % $Param{Children} ] }, $ArticleID;
}
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');
my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
my %ActiveChildPID;
ARTICLEIDCHUNK:
for my $ArticleIDChunk (@ArticleChunks) {
# Create a child process.
my $PID = fork;
# Could not create child.
if ( $PID < 0 ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Unable to fork to a child process for article index rebuild!"
);
last ARTICLEIDCHUNK;
}
# We're in the child process.
if ( !$PID ) {
# Add the chunk of article data to the index.
for my $ArticleID ( @{$ArticleIDChunk} ) {
my $Success = 0;
if (
$ConfigObject->Get('Ticket::ArchiveSystem')
&& !$ConfigObject->Get('Ticket::SearchIndex::IndexArchivedTickets')
&& $TicketObject->TicketArchiveFlagGet( TicketID => $Param{ArticleTicketIDs}->{$ArticleID} )
)
{
$Success = $ArticleObject->ArticleSearchIndexDelete(
ArticleID => $ArticleID,
UserID => 1,
);
}
else {
$Success = $ArticleObject->ArticleSearchIndexBuild(
TicketID => $Param{ArticleTicketIDs}->{$ArticleID},
ArticleID => $ArticleID,
UserID => 1,
);
}
if ( !$Success ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Could not rebuild index for ArticleID '$ArticleID'!"
);
}
else {
$ArticleObject->ArticleSearchIndexRebuildFlagSet(
ArticleIDs => [$ArticleID],
Value => 0,
);
}
}
# Close child process at the end.
exit 0;
}
$ActiveChildPID{$PID} = {
PID => $PID,
};
}
# Check the status of all child processes every 0.1 seconds.
# Wait for all child processes to be finished.
WAIT:
while (1) {
last WAIT if !%ActiveChildPID;
sleep 0.1;
PID:
for my $PID ( sort keys %ActiveChildPID ) {
my $WaitResult = waitpid( $PID, WNOHANG );
if ( $WaitResult == -1 ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Child process exited with errors: $?",
);
delete $ActiveChildPID{$PID};
next PID;
}
delete $ActiveChildPID{$PID} if $WaitResult;
}
}
return 1;
}
sub PostRun {
my ($Self) = @_;
my $Success = $Kernel::OM->Get('Kernel::System::PID')->PIDDelete(
Name => 'ArticleSearchIndexRebuild',
);
if ( !$Success ) {
$Self->PrintError("Unable to unregister indexing process! Skipping...\n");
return $Self->ExitCodeError();
}
return $Success;
}
1;