# -- # 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;