http://www.mediawiki.org/wiki/Special:Code/MediaWiki/90068
Revision: 90068
Author: dale
Date: 2011-06-14 18:17:24 +0000 (Tue, 14 Jun 2011)
Log Message:
-----------
bug 29184 Adds background transcoding support.
$wgEnableNiceBackgroundTranscodeJobs configuration option enables:
* Global Timeout, don't let any transcode go on indefinitely
* Growing target files, don't let transcodes not do any work for more than 10
seconds.
* Enables us to set lower resource priority on transcodes to keep
multi-threaded transcode boxes responsive for other tasks ( like monitoring
transcodes )
TODO figure out a way to get exit status and PID for background tasks ( some
notes in the header )
Modified Paths:
--------------
trunk/extensions/TimedMediaHandler/TimedMediaHandler.php
trunk/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscode.php
trunk/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscodeJob.php
trunk/extensions/TimedMediaHandler/maintenance/WebVideoJobRunner.php
Modified: trunk/extensions/TimedMediaHandler/TimedMediaHandler.php
===================================================================
--- trunk/extensions/TimedMediaHandler/TimedMediaHandler.php 2011-06-14
18:06:36 UTC (rev 90067)
+++ trunk/extensions/TimedMediaHandler/TimedMediaHandler.php 2011-06-14
18:17:24 UTC (rev 90068)
@@ -52,6 +52,19 @@
// the remote repo has transcoding enabled and associated flavors for that
media embed.
$wgEnableTranscode = true;
+// If the job runner should run transcode commands in a background thread and
monitor the
+// transcoding progress. This enables more fine grain control of the
transcoding process, wraps
+// encoding commands in a lower priority 'nice' call, and kills long running
transcodes that are
+// not making any progress. If set to false, the job runner will use the more
compatible
+// php blocking shell exec command.
+$wgEnableNiceBackgroundTranscodeJobs = true;
+
+// The priority to be used with the nice transcode commands.
+$wgTranscodeBackgroundPriority = 19;
+
+// The total amount of time a transcoding shell command can take:
+$wgTranscodeBackgroundTimeLimit = 3600 * 4;
+
// The location of ffmpeg2theora ( transcoding )
$wgFFmpeg2theoraLocation = '/usr/bin/ffmpeg2theora';
Modified:
trunk/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscode.php
===================================================================
--- trunk/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscode.php
2011-06-14 18:06:36 UTC (rev 90067)
+++ trunk/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscode.php
2011-06-14 18:17:24 UTC (rev 90068)
@@ -154,7 +154,7 @@
// in-progress encodes, its nice having it publicly accessible
for debugging though
$filePath = self::getDerivativeFilePath( $file, $transcodeKey );
$ext = strtolower( pathinfo( "$filePath", PATHINFO_EXTENSION )
);
- return "{$filePath}.tmp.{$ext}";
+ return "{$filePath}.part.{$ext}";
}
/**
Modified:
trunk/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscodeJob.php
===================================================================
---
trunk/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscodeJob.php
2011-06-14 18:06:36 UTC (rev 90067)
+++
trunk/extensions/TimedMediaHandler/WebVideoTranscode/WebVideoTranscodeJob.php
2011-06-14 18:17:24 UTC (rev 90068)
@@ -15,7 +15,37 @@
*
* @ingroup JobQueue
*/
+
+
+/**
+ * Unless I am unaware of some command line trickery, we have to wrap the
background
+ * commands in a script to grab the background exit code ( echo $? )
+ *
+ * Ie in the bellow code "echo $?" gives us the status of "echo $1" not
"nohub" call
+ * a wait $pid & echo $? of course does not work since the background taks is
not part
+ * of a child shell call
+ *
+ * pseudo shell code for backgroundTask.sh
+ *
+#/bin/bash
+
+# simple script to run a background and writing output and exit status to a
file
+# returns its "pid" for proccess monitoring
+# Usage: backgroundTask.sh [command] [stdout target file] [pid target file]
[exit status target file]
+
+# Run the command with output redirected to target file ( in the background )
+$1 > $2 &
+# Output the pid to target file:
+echo $! > $3
+# wait until task is done
+wait $!
+# output exit status:
+echo $? > $4
+
+*/
class WebVideoTranscodeJob extends Job {
+ var $targetEncodePath = null;
+ var $sourceFilePath = null;
public function __construct( $title, $params, $id = 0 ) {
parent::__construct( 'webVideoTranscode', $title, $params, $id
);
@@ -25,25 +55,40 @@
private function output( $msg ){
print $msg . "\n";
}
-
+ private function getTargetEncodePath(){
+ if( !$this->targetEncodePath ){
+ $file = wfLocalFile( $this->title );
+ $transcodeKey = $this->params['transcodeKey'];
+ $this->targetEncodePath =
WebVideoTranscode::getTargetEncodePath( $file, $transcodeKey );
+ }
+ return $this->targetEncodePath;
+ }
+ private function getSourceFilePath(){
+ if( !$this->sourceFilePath ){
+ $file = wfLocalFile( $this->title );
+ $this->sourceFilePath = $file->getPath();
+ }
+ return $this->sourceFilePath;
+ }
// Run the transcode request
public function run() {
- // Get the file object
- $file = wfLocalFile( $this->title );
+ // get a local pointer to the file
+ $file = wfLocalFile( $this->title );
- $source = $file->getPath();
- if( !is_file($source ) ){
+ // Validate the file exists :
+ if( !$file || !is_file( $this->getSourceFilePath() ) ){
$this->output( 'File not found: ' . $this->title );
return false;
}
+
+ // Validate the transcode key param:
$transcodeKey = $this->params['transcodeKey'];
-
// Build the destination target
- $destinationFile = WebVideoTranscode::getTargetEncodePath(
$file, $transcodeKey );
if( ! isset( WebVideoTranscode::$derivativeSettings[
$transcodeKey ] )){
$this->output( "Transcode key $transcodeKey not found,
skipping" );
return false;
}
+
$options = WebVideoTranscode::$derivativeSettings[
$transcodeKey ];
$this->output( "Encoding to codec: " . $options['videoCodec'] );
@@ -79,32 +124,26 @@
// Check the codec see which encode method to call;
if( $options['videoCodec'] == 'theora' ){
- $status = $this->ffmpeg2TheoraEncode( $file,
$destinationFile, $options );
+ $status = $this->ffmpeg2TheoraEncode( $options );
} else if( $options['videoCodec'] == 'vp8' ){
// Check for twopass:
if( isset( $options['twopass'] ) ){
// ffmpeg requires manual two pass
- $status = $this->ffmpegEncode( $file,
$destinationFile, $options, 1 );
+ $status = $this->ffmpegEncode( $options, 1 );
if( $status ){
- $status = $this->ffmpegEncode( $file,
$destinationFile, $options, 2 );
- // unlink the .log file used in two
pass encoding:
- wfSuppressWarnings();
- unlink( $destinationFile . '.log' );
- // Sometimes ffmpeg gives the file
log-0.log extension
- unlink( $destinationFile . 'log-0.log');
- wfRestoreWarnings();
- }
- // remove any log files
- $this->removeFffmpgeLogFiles( dirname(
$destinationFile) );
-
+ $status = $this->ffmpegEncode(
$options, 2 );
+ }
} else {
- $status = $this->ffmpegEncode( $file,
$destinationFile, $options );
+ $status = $this->ffmpegEncode( $options );
}
} else {
wfDebug( 'Error unknown codec:' . $options['codec'] );
$status = 'Error unknown target encode codec:' .
$options['codec'];
}
+ // Remove any log files all useful info should be in status and
or we are done with 2 passs encoding
+ $this->removeFffmpgeLogFiles();
+
// Do a quick check to confirm the job was not restarted or
removed while we were transcoding
// Confirm the in memory $jobStartTimeCache matches db start
time
$dbStartTime = $db->selectField( 'transcode',
'transcode_time_startwork',
@@ -128,7 +167,7 @@
if( $status === true ){
$finalDerivativeFilePath =
WebVideoTranscode::getDerivativeFilePath( $file, $transcodeKey);
wfSuppressWarnings();
- $status = rename( $destinationFile,
$finalDerivativeFilePath );
+ $status = rename( $this->getTargetEncodePath(),
$finalDerivativeFilePath );
wfRestoreWarnings();
$bitrate = round( intval( filesize(
$finalDerivativeFilePath ) / $file->getLength() ) * 8 );
// Update the transcode table with success time:
@@ -171,12 +210,13 @@
// pass along result status:
return $status;
}
- function removeFffmpgeLogFiles( $dir ){
+ function removeFffmpgeLogFiles(){
+ $dir = dirname( $this->getTargetEncodePath() );
if (is_dir($dir)) {
if ($dh = opendir($dir)) {
while (($file = readdir($dh)) !== false) {
$ext = strtolower(
pathinfo("$dir/$file", PATHINFO_EXTENSION) );
- if( $ext == '.log' ){
+ if( $ext == 'log' ){
wfSuppressWarnings();
unlink( "$dir/$file");
wfRestoreWarnings();
@@ -187,14 +227,11 @@
}
}
/** Utility helper for ffmpeg and ffmpeg2theora mapping **/
- function ffmpegEncode( $file, $target, $options, $pass=0 ){
+ function ffmpegEncode( $options, $pass=0 ){
global $wgFFmpegLocation;
- // Get the source
- $source = $file->getPath();
- $this->output( "Encode:\n source:$source\n target:$target\n" );
-
+
// Set up the base command
- $cmd = wfEscapeShellArg( $wgFFmpegLocation ) . ' -i ' .
wfEscapeShellArg( $source );
+ $cmd = wfEscapeShellArg( $wgFFmpegLocation ) . ' -i ' .
wfEscapeShellArg( $this->getSourceFilePath() );
if( isset($options['preset']) ){
if ($options['preset'] == "360p") {
@@ -208,7 +245,7 @@
if ( isset( $options['novideo'] ) ) {
$cmd.= " -vn ";
} else {
- $cmd.= $this->ffmpegAddVideoOptions( $file, $target,
$options, $pass );
+ $cmd.= $this->ffmpegAddVideoOptions( $options, $pass );
}
// Check for start time
@@ -219,13 +256,13 @@
}
// Check for end time:
if( isset( $options['endtime'] ) ){
- $cmd.= ' -t ' . intval( $options['endtime'] ) -
intval($options['starttime'] ) ;
+ $cmd.= ' -t ' . intval( $options['endtime'] ) -
intval($options['starttime'] ) ;
}
if ( $pass == 1 || isset( $options['noaudio'] ) ) {
- $cmd.= ' -an';
+ $cmd.= ' -an';
} else {
- $cmd.= $this->ffmpegAddAudioOptions( $file, $target, $options,
$pass );
+ $cmd.= $this->ffmpegAddAudioOptions( $options, $pass );
}
// Output WebM
@@ -233,31 +270,33 @@
if ( $pass != 0 ) {
$cmd.=" -pass " .wfEscapeShellArg( $pass ) ;
- $cmd.=" -passlogfile " . wfEscapeShellArg( $target
.'.log' );
+ $cmd.=" -passlogfile " . wfEscapeShellArg(
$this->getTargetEncodePath() .'.log' );
}
// And the output target:
if ($pass==1) {
$cmd.= ' /dev/null';
} else{
- $cmd.= " $target";
+ $cmd.= " " . $this->getTargetEncodePath();
}
- // Don't display shell output
- $cmd .= ' 2>&1';
-
$this->output( "Running cmd: \n\n" .$cmd . "\n" );
// Right before we output remove the old file
wfProfileIn( 'ffmpeg_encode' );
- $shellOutput = wfShellExec( $cmd, $retval );
+ $retval = 0;
+ $shellOutput = $this->runShellExec( $cmd, $retval );
wfProfileOut( 'ffmpeg_encode' );
- if( $retval ){
+ if( $retval != 0 ){
return $cmd . "\n\n" . $shellOutput;
}
return true;
}
- function ffmpegAddVideoOptions( $file, $target, $options, $pass){
+ function ffmpegAddVideoOptions( $options, $pass){
+
+ // Get a local pointer to the file object
+ $file = wfLocalFile( $this->title );
+
$cmd ='';
// Add the boiler plate vp8 ffmpeg command:
$cmd.=" -y -skip_threshold 0 -rc_buf_aggressivity 0 -bufsize
6000k -rc_init_occupancy 4000 -threads 4";
@@ -322,7 +361,7 @@
return $cmd;
}
- function ffmpegAddAudioOptions( $file, $target, $options, $pass){
+ function ffmpegAddAudioOptions( $options, $pass){
$cmd ='';
if( isset( $options['audioQuality'] ) ){
$cmd.= " -aq " . wfEscapeShellArg(
$options['audioQuality'] );
@@ -346,14 +385,11 @@
/**
* ffmpeg2Theora mapping is much simpler since it is the basis of the
the firefogg API
*/
- function ffmpeg2TheoraEncode( $file, $target, $options){
+ function ffmpeg2TheoraEncode( $options){
global $wgFFmpeg2theoraLocation;
- // Get the source:
- $source = $file->getPath();
-
// Set up the base command
- $cmd = wfEscapeShellArg( $wgFFmpeg2theoraLocation ) . ' ' .
wfEscapeShellArg( $source );
+ $cmd = wfEscapeShellArg( $wgFFmpeg2theoraLocation ) . ' ' .
wfEscapeShellArg( $this->getSourceFilePath() );
// Add in the encode settings
foreach( $options as $key => $val ){
if( isset( self::$foggMap[$key] ) ){
@@ -371,22 +407,130 @@
}
// Add the output target:
- $cmd.= ' -o ' . wfEscapeShellArg ( $target );
+ $cmd.= ' -o ' . wfEscapeShellArg ( $this->getTargetEncodePath()
);
- // Don't display shell output
- $cmd.=' 2>&1';
-
$this->output( "Running cmd: \n\n" .$cmd . "\n" );
wfProfileIn( 'ffmpeg2theora_encode' );
- $shellOutput = wfShellExec( $cmd, $retval );
+ $retval = 0;
+ $shellOutput = $this->runShellExec( $cmd, $retval );
wfProfileOut( 'ffmpeg2theora_encode' );
-
- if( $retval ){
+ if( $retval != 0 ){
return $cmd . "\n\n" . $shellOutput;
}
return true;
}
+ /**
+ * Runs the shell exec command.
+ * if $wgEnableBackgroundTranscodeJobs is enabled will mannage a
background transcode task
+ * else it just directly passes off to wfShellExec
+ *
+ * @param $cmd String Command to be run
+ * @param $retval String, refrence variable to return the exit code
+ */
+ public function runShellExec( $cmd, &$retval){
+ global $wgEnableNiceBackgroundTranscodeJobs,
$wgTranscodeBackgroundPriority,
+ $wgTranscodeBackgroundTimeLimit;
+
+ // Check if background tasks are enabled
+ if( $wgEnableNiceBackgroundTranscodeJobs === false ){
+ // Dont display shell output
+ $cmd .= ' 2>&1';
+ // Directly execute the shell command:
+ return wfShellExec( $cmd, $retval );
+ }
+ // Setup pointers for status and encoding log:
+ $encodingLog = $this->getTargetEncodePath() . '.stdout.log';
+ $exitStatusLog = $this->getTargetEncodePath() . '.exit.log';
+
+ // Start background shell proc
+ $pid = wfShellExec(
+ "nohup nice -n $wgTranscodeBackgroundPriority $cmd > "
. wfEscapeShellArg( $encodingLog ) . ' ' .
+ // ideally we could store the exit status here ( but it
causes & echo $! to give us a bad pid )
+ // "& echo $? > " . wfEscapeShellArg( $exitStatusLog )
. ' ' .
+ "& echo $!",
+ $retval
+ );
+ $pid = trim( $pid );
+ $errorMsg = '';
+
+ if( $pid == '' || $retval != 0){
+ $errorMsg = "Failed to start, check $wgMaxShellMemory
settings";
+ $this->output( $errorMsg);
+ $retval = 1;
+ return $errorMsg;
+ }
+ $loopCount = 0;
+ $oldFileSize = 0;
+ $startTime = time();
+ $fileIsNotGrowing = false;
+
+ while( true ){
+
+ // Check if pid still runing
+ if( ! self::isProcessRunning( $pid ) ){
+ // see large note above about background tasks
( echo $? is probably not the exit status we want )
+ $retval = wfShellExec( "echo $?");
+ //$this->output( $pid . ' is done, $retval: '
.$retval );
+ break;
+ }
+
+ // Check that the target file is growing ( every 5
seconds )
+ if( $loopCount == 5 ){
+ // only run check if we are outputing to target
file
+ // ( two pass encoding does not output to
target on first pass )
+ if( is_file( $this->getTargetEncodePath() ) ){
+ clearstatcache();
+ $newFileSize = filesize(
$this->getTargetEncodePath() );
+ if( $newFileSize == $oldFileSize ){
+ if( $fileIsNotGrowing ){
+ $errorMsg = "Target
File is not increasing in size, kill process.";
+ $this->output(
$errorMsg );
+ // file is not growing
in size, kill proccess
+ $retval = 1;
+ self::killProcess( $pid
);
+ break;
+ }
+ // Wait an additional 5 seconds
of the file not growing to confirm
+ // the transcode is frozen.
+ $fileIsNotGrowing = true;
+ }
+ $oldFileSize = $newFileSize;
+ }
+ // reset the loop counter
+ $loopCount = 0;
+ }
+
+ // Check if we have global job run-time has been
exceeded:
+ if ( $wgTranscodeBackgroundTimeLimit && time() -
$startTime > $wgTranscodeBackgroundTimeLimit ){
+ $errorMsg = "Encoding exceeded max job run time
( " . TimedMediaHandler::seconds2npt( $maxTime ) . " ), kill process.";
+ $this->output( $errorMsg );
+ // File is not growing in size, kill proccess
+ $retval = 1;
+ self::killProcess( $pid );
+ break;
+ }
+
+ // Sleep for one second before repeating loop
+ $loopCount++;
+ sleep( 1 );
+ }
+
+ // return the encoding log contents ( will be inserted into
error table if an error )
+ // ( will be ignored and removed if success )
+ if( $errorMsg!= '' ){
+ $errorMsg.="\n\n";
+ }
+ return $errorMsg . file_get_contents( $encodingLog );
+ }
+ public static function isProcessRunning( $pid ){
+ exec( "ps $pid", $processState );
+ return( count( $processState ) >= 2 );
+ }
+ public static function killProcess( $pid ){
+ // we are killing a hung prcoces send -9 signal
+ wfShellExec( "kill -9 $pid");
+ }
/**
* Mapping between firefogg api and ffmpeg2theora command line
Modified: trunk/extensions/TimedMediaHandler/maintenance/WebVideoJobRunner.php
===================================================================
--- trunk/extensions/TimedMediaHandler/maintenance/WebVideoJobRunner.php
2011-06-14 18:06:36 UTC (rev 90067)
+++ trunk/extensions/TimedMediaHandler/maintenance/WebVideoJobRunner.php
2011-06-14 18:17:24 UTC (rev 90068)
@@ -15,11 +15,7 @@
// Default number of simultaneous transcoding threads
var $threads = 2;
- // Default time for a transcode to time out: 4 hours,
- // ( should be increased if we add long form content to wikimedia )
- var $transcodeTimeout = 14400;
-
- // How often the script checks for $threads transcode jobs to be
active.
+ // How often ( in seconds ) the script checks for $threads transcode
jobs to be active.
var $checkJobsFrequency = 5;
public function __construct() {
@@ -51,7 +47,7 @@
}
}
function runCheckJobThreadsLoop(){
- global $wgMaintenancePath;
+ global $wgMaintenancePath, $wgTranscodeBackgroundTimeLimit;
// Check if we have $threads number of webTranscode jobs
running else sleep
$runingJobsCount = 0;
foreach( $this->getProcessList() as $pid => $proc ){
@@ -60,6 +56,7 @@
strpos( $proc['args'], '--type
webVideoTranscode' ) !== false
){
if( TimedMediaHandler::parseTimeString(
$proc['time'] ) > $this->transcodeTimeout ){
+ // should probably "kill" the process
} else {
// Job is oky add to count:
@@ -69,7 +66,7 @@
}
if( $runingJobsCount < $this->threads ){
// Add one process:
- $cmd = "php $wgMaintenancePath/runJobs.php --type
webVideoTranscode --maxjobs 1 --maxtime {$this->transcodeTimeout}";
+ $cmd = "php $wgMaintenancePath/runJobs.php --type
webVideoTranscode --maxjobs 1 --maxtime {$wgTranscodeBackgroundTimeLimit}";
$status = $this->runBackgroundProc( $cmd );
$this->output( "$runingJobsCount existing job runners,
Check for new transcode jobs: " );
} else {
_______________________________________________
MediaWiki-CVS mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs