Skip to content

Commit 3f5646d

Browse files
committed
Added protection for "svn log ... --use-merge-history ..." command, that might hand on large merges
1 parent 5cdab4a commit 3f5646d

File tree

5 files changed

+114
-9
lines changed

5 files changed

+114
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
88

99
### Changed
1010
- Show executed SVN commands in real time (when started; how long was executed) in verbose mode (the `-v` flag).
11+
- The executed SVN command idle timeout changed from 3 minutes to 1 minute.
1112

1213
### Fixed
13-
...
14+
- Handle cases, when `svn log ... --use-merge-history ...` command timeout-out.
1415

1516
## [0.8.0] - 2024-12-18
1617
### Added

src/SVNBuddy/Repository/Connector/Command.php

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
use ConsoleHelpers\SVNBuddy\Process\IProcessFactory;
1818
use Symfony\Component\Console\Output\OutputInterface;
1919
use Symfony\Component\Process\Exception\ProcessFailedException;
20+
use Symfony\Component\Process\Exception\ProcessTimedOutException;
2021
use Symfony\Component\Process\Process;
2122

2223
class Command
2324
{
2425

25-
const IDLE_TIMEOUT = 180; // 3 minutes.
26+
const IDLE_TIMEOUT = 60; // 1 minute.
2627

2728
/**
2829
* Process factory.
@@ -73,6 +74,13 @@ class Command
7374
*/
7475
private $_cacheOverwrite = false;
7576

77+
/**
78+
* Indicates whether idle timeout recovery is enabled.
79+
*
80+
* @var boolean
81+
*/
82+
private $_idleTimeoutRecovery = false;
83+
7684
/**
7785
* Creates a command instance.
7886
*
@@ -135,6 +143,20 @@ public function setCacheOverwrite($cache_overwrite)
135143
return $this;
136144
}
137145

146+
/**
147+
* Set idle timeout recovery.
148+
*
149+
* @param boolean $idle_timeout_recovery Idle timeout recovery.
150+
*
151+
* @return self
152+
*/
153+
public function setIdleTimeoutRecovery($idle_timeout_recovery)
154+
{
155+
$this->_idleTimeoutRecovery = $idle_timeout_recovery;
156+
157+
return $this;
158+
}
159+
138160
/**
139161
* Runs the command.
140162
*
@@ -212,6 +234,7 @@ private function _getCacheKey()
212234
*
213235
* @return string
214236
* @throws RepositoryCommandException When command execution failed.
237+
* @throws ProcessTimedOutException When process timed-out with general timeout type.
215238
*/
216239
private function _doRun($callback = null)
217240
{
@@ -236,13 +259,15 @@ private function _doRun($callback = null)
236259
$process->mustRun($callback);
237260
}
238261

239-
$output = (string)$process->getOutput();
240-
241-
if ( $this->_io->isDebug() ) {
242-
$this->_io->writeln($output, OutputInterface::OUTPUT_RAW);
262+
return $this->getProcessOutput($process);
263+
}
264+
catch ( ProcessTimedOutException $e ) {
265+
// This happens for "svn log --use-merge-history ..." command when we've got all the output already.
266+
if ( $this->_idleTimeoutRecovery && $e->isIdleTimeout() ) {
267+
return $this->getProcessOutput($process);
243268
}
244269

245-
return $output;
270+
throw $e;
246271
}
247272
catch ( ProcessFailedException $e ) {
248273
throw new RepositoryCommandException(
@@ -252,6 +277,24 @@ private function _doRun($callback = null)
252277
}
253278
}
254279

280+
/**
281+
* Returns process output.
282+
*
283+
* @param Process $process Process.
284+
*
285+
* @return string
286+
*/
287+
protected function getProcessOutput(Process $process)
288+
{
289+
$output = (string)$process->getOutput();
290+
291+
if ( $this->_io->isDebug() ) {
292+
$this->_io->writeln($output, OutputInterface::OUTPUT_RAW);
293+
}
294+
295+
return $output;
296+
}
297+
255298
/**
256299
* Runs an svn command and displays output in real time.
257300
*

src/SVNBuddy/Repository/RevisionLog/RevisionLog.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ private function _useRepositoryCollectorPlugins($from_revision, $to_revision, $o
302302
$log_command_arguments
303303
);
304304
$command = $this->_repositoryConnector->getCommand('log', $command_arguments);
305-
$command->setCacheDuration($cache_duration);
305+
$command->setCacheDuration($cache_duration)->setIdleTimeoutRecovery(true);
306306
$svn_log = $command->run();
307307

308308
$this->_parseLog($svn_log, $plugins);

tests/SVNBuddy/Repository/Connector/CommandTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Prophecy\Prophecy\ObjectProphecy;
2121
use Symfony\Component\Console\Output\OutputInterface;
2222
use Symfony\Component\Process\Exception\ProcessFailedException;
23+
use Symfony\Component\Process\Exception\ProcessTimedOutException;
2324
use Symfony\Component\Process\Process;
2425
use Tests\ConsoleHelpers\SVNBuddy\AbstractTestCase;
2526

@@ -126,6 +127,65 @@ public static function runWithoutCachingDataProvider()
126127
);
127128
}
128129

130+
/**
131+
* @dataProvider runWithIdleTimeoutRecoveryDataProvider
132+
*/
133+
public function testRunWithIdleTimeoutRecovery($use_recovery, $timeout_type)
134+
{
135+
$command_line = array('svn', 'log', '--xml');
136+
$process_output = '<log><logentry/></log>';
137+
138+
$this->_command = $this->_createCommand($command_line);
139+
140+
if ( $use_recovery ) {
141+
$this->_command->setIdleTimeoutRecovery(true);
142+
}
143+
144+
$this->_process->getCommandLine()
145+
->willReturn(implode(' ', $command_line))
146+
->shouldBeCalled();
147+
148+
if ( $timeout_type === ProcessTimedOutException::TYPE_IDLE ) {
149+
$this->_process->getIdleTimeout()->willReturn(123)->shouldBeCalled();
150+
}
151+
else {
152+
$this->_process->getTimeout()->willReturn(123)->shouldBeCalled();
153+
}
154+
155+
$process_timed_out_exception = new ProcessTimedOutException(
156+
$this->_process->reveal(),
157+
$timeout_type
158+
);
159+
160+
$this->_process->mustRun(null)
161+
->willThrow($process_timed_out_exception)
162+
->shouldBeCalled();
163+
164+
$this->_cacheManager->getCache(Argument::any())->shouldNotBeCalled();
165+
166+
if ( $use_recovery ) {
167+
$this->_process->getOutput()->willReturn($process_output)->shouldBeCalled();
168+
$this->_io->isVerbose()->willReturn(false)->shouldBeCalled();
169+
$this->_io->isDebug()->willReturn(false)->shouldBeCalled();
170+
}
171+
else {
172+
$this->expectException(get_class($process_timed_out_exception));
173+
$this->expectExceptionMessage($process_timed_out_exception->getMessage());
174+
}
175+
176+
$this->assertCommandOutput(null, true, $process_output);
177+
}
178+
179+
public static function runWithIdleTimeoutRecoveryDataProvider()
180+
{
181+
return array(
182+
'with recovery+idle timeout' => array(true, ProcessTimedOutException::TYPE_IDLE),
183+
'with recovery+general timeout' => array(true, ProcessTimedOutException::TYPE_IDLE),
184+
'without recovery+idle timeout' => array(false, ProcessTimedOutException::TYPE_GENERAL),
185+
'without recovery+general timeout' => array(false, ProcessTimedOutException::TYPE_GENERAL),
186+
);
187+
}
188+
129189
/**
130190
* @dataProvider runWithCacheDataProvider
131191
*/

tests/SVNBuddy/Repository/RevisionLog/RevisionLogTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,8 @@ protected function expectRepositoryCommand($command_name, array $arguments, $res
459459

460460
$command = $this->prophesize(Command::class);
461461
$command->run()->willReturn($result)->shouldBeCalled();
462-
$command->setCacheDuration('10 years')->shouldBeCalled();
462+
$command->setCacheDuration('10 years')->willReturn($command)->shouldBeCalled();
463+
$command->setIdleTimeoutRecovery(true)->willReturn($command)->shouldBeCalled();
463464

464465
$this->repositoryConnector->getCommand($command_name, $arguments)->willReturn($command)->shouldBeCalled();
465466
}

0 commit comments

Comments
 (0)