5

I'm writing a queueable class to perform some image optimizations. The class has to call out to AWS S3, get the source image, then call out to Kraken.io three times to optimize the source image, get a 1024x1024-ish resized image, and a 200x200-ish thumbnail image. The individual service classes for AWS S3 and Kraken.io have been tested and work perfectly. The queueable class also works fine when not invoked as a queued job, but when it's a queued job it fails.

The issue currently is that I didn't have the static methods annotated with @Future (Callout = true). When I did annotate them I got an error that I can't be returning anything but void. I also can't pass objects as parameters. So, right now I'm stuck on what to do. I can't use static variables because if I queue up more than one job, the variables are no longer guaranteed to contain the appropriate information at any given point.

I'd appreciate any suggestions on how I can continue ahead.

Here is my code. I have a Scheduleable class up front to queue up the jobs which self schedules every 5 minutes.

vNImageOptimizationScheduler

public without sharing class vNImageOptimizationScheduler implements Schedulable {
    public void execute(
        SchedulableContext context) {
        //  Get a list of objects that need to be processed
        List<vNOpportunityPhoto__c> photos = [
            SELECT  Id,
                    IsOptimized__c,
                    IsQueued__c,
                    ResizedFile__c,
                    SourceFile__r.Extension__c,
                    SourceFile__r.FileName__c,
                    SourceFile__r.FileSize__c,
                    SourceFile__r.S3Key__c,
                    ThumbnailFile__c
            FROM    vNOpportunityPhoto__c
            WHERE   IsOptimized__c = false
                    AND (IsQueued__c = false)
            LIMIT   20
        ];

        //  Queue all objects as jobs
        for (vNOpportunityPhoto__c photo : photos) {
            photo.IsQueued__c = true;

            System.enqueueJob(new vNImageOptimizationJob(photo));
        }

        //  Update all queued objects
        update photos;

        datetime next = System.now().addSeconds(300);
        string second = string.valueOf(next.second()),
               minute = string.valueOf(next.minute()),
               hour = string.valueOf(next.hour()),
               day = string.valueOf(next.day()),
               month = string.valueOf(next.month()),
               year = string.valueOf(next.year()),
               expression = second + ' ' + minute + ' ' + hour + ' ' + day + ' ' + month + ' ? ' + year,
               job = 'vNImageOptimizationScheduler ' + next.format('hh:mm:ss');

        if (!Test.isRunningTest()) {
            System.schedule(expression, job, new vNImageOptimizationScheduler());
            System.abortJob(context.getTriggerId());
        }
    }
}

vNImageOptimizationJob

public without sharing class vNImageOptimizationJob implements Queueable {
    private static final vNAmazonS3Service AwsService = new vNAmazonS3Service('AccessKey', 'SecretKey', 'Region');
    private static final vNKrakenService KrakenService = new vNKrakenService('AccessKey', 'SecretKey');

    private final vNOpportunityPhoto__c Photo { get; set; }

    public vNImageOptimizationJob(
        vNOpportunityPhoto__c photo) {
        System.debug('Optimizing file: ' + Photo.SourceFile__r.FileName__c);

        this.Photo = photo;
    }

    public void execute(
        QueueableContext context) {
        blob fileBlob = DownloadS3File(Photo);

        if (fileBlob == null) {
            System.debug('Source file from S3 is null, bailing out.');

            return;
        }

        OptimizationResult sourceOptimizationResult = GetOptimizedSource(Photo, fileBlob);

        if (sourceOptimizationResult == null) {
            System.debug('Source optimization failed, bailing out.');

            return;
        } else {
            fileBlob = null;//  dispose of the original blob early since it's no longer needed
        }

        OptimizationResult resizeOptimizationResult = GetOptimizedResize(Photo, sourceOptimizationResult.FileBlob);

        if (resizeOptimizationResult == null) {
            System.debug('Resize optimization failed, bailing out.');

            return;
        }

        OptimizationResult thumbnailOptimizationResult = GetOptimizedThumbnail(Photo, sourceOptimizationResult.FileBlob);

        if (thumbnailOptimizationResult == null) {
            System.debug('Thumbnail optimization failed, bailing out.');

            return;
        }

        boolean sourcePutResult = PutOptimizedBlob(sourceOptimizationResult);

        if (!sourcePutResult) {
            System.debug('Source put failed, bailing out.');

            return;
        }

        boolean resizePutResult = PutOptimizedBlob(resizeOptimizationResult);

        if (!resizePutResult) {
            System.debug('Resize put failed, bailing out.');

            return;
        }

        boolean thumbnailPutResult = PutOptimizedBlob(thumbnailOptimizationResult);

        if (!thumbnailPutResult) {
            System.debug('Thumbnail put failed, bailing out.');

            return;
        }

        vNFile__c resizedFile = new vNFile__c();

        resizedFile.Extension__c = photo.SourceFile__r.Extension__c;
        resizedFile.FileName__c = resizeOptimizationResult.KeyName;
        resizedFile.FileSize__c = resizeOptimizationResult.FileSize;
        resizedFile.S3Key__c = resizeOptimizationResult.KeyName;

        insert resizedFile;

        vNFile__c thumbnailFile = new vNFile__c();

        thumbnailFile.Extension__c = photo.SourceFile__r.Extension__c;
        thumbnailFile.FileName__c = thumbnailOptimizationResult.KeyName;
        thumbnailFile.FileSize__c = thumbnailOptimizationResult.FileSize;
        thumbnailFile.S3Key__c = thumbnailOptimizationResult.KeyName;

        insert thumbnailFile;

        Photo.IsOptimized__c = true;
        Photo.ResizedFile__c = resizedFile.Id;
        Photo.SourceFile__r.FileSize__c = sourceOptimizationResult.FileSize;
        Photo.ThumbnailFile__c = thumbnailFile.Id;

        update Photo;
        update Photo.SourceFile__r;
    }

    // @Future (Callout = true)
    private static blob DownloadS3File(
        vNOpportunityPhoto__c photo) {
        vNAmazonS3Service.GetObjectRequest getObjectRequest = new vNAmazonS3Service.GetObjectRequest();

        getObjectRequest.BucketName = 'Bucket';
        getObjectRequest.KeyName = photo.SourceFile__r.S3Key__c;

        return AwsService.GetObject(getObjectRequest).FileBlob;
    }

    // @Future (Callout = true)
    private static blob DownloadKrakenFile(
        vNKrakenService.KrakenResponse response) {
        HttpRequest httpRequest = new HttpRequest();

        httpRequest.setEndpoint(response.KrakedUrl);
        httpRequest.setMethod('GET');
        httpRequest.setTimeout(120000);

        HttpResponse httpResponse = new Http().send(httpRequest);

        return httpResponse.getBodyAsBlob();
    }

    private static OptimizationResult GetOptimizedResize(
        vNOpportunityPhoto__c photo,
        blob fileBlob) {
        string keyName = vNUtilities.GetRandomString(32) + photo.SourceFile__r.Extension__c;
        vNKrakenService.OptimizeWaitRequest optimizeWaitRequest = new vNKrakenService.OptimizeWaitRequest();

        optimizeWaitRequest.AutoOrient = true;
        optimizeWaitRequest.FileBlob = fileBlob;
        optimizeWaitRequest.FileName = keyName;
        optimizeWaitRequest.Lossy = true;
        optimizeWaitRequest.Resize = new vNKrakenService.KrakenResize();
        optimizeWaitRequest.Resize.Height = 1024;
        optimizeWaitRequest.Resize.Width = 1024;

        vNKrakenService.KrakenResponse optimizeWaitResponse = KrakenService.OptimizeWait(optimizeWaitRequest);

        if (!optimizeWaitResponse.Success) {
            System.debug('Resize optimization failed, error: ' + optimizeWaitResponse.Error);

            return null;
        }

        OptimizationResult result = new OptimizationResult();

        result.FileBlob = DownloadKrakenFile(optimizeWaitResponse);
        result.FileSize = optimizeWaitResponse.KrakedSize;
        result.KeyName = keyName;

        return result;
    }

    private static OptimizationResult GetOptimizedSource(
        vNOpportunityPhoto__c photo,
        blob fileBlob) {
        string keyName = photo.SourceFile__r.S3Key__c;
        vNKrakenService.OptimizeWaitRequest optimizeWaitRequest = new vNKrakenService.OptimizeWaitRequest();

        optimizeWaitRequest.AutoOrient = true;
        optimizeWaitRequest.FileBlob = fileBlob;
        optimizeWaitRequest.FileName = keyName;
        optimizeWaitRequest.Lossy = true;
        optimizeWaitRequest.PreserveMetadata.add('date');//         shortcutting to reduce governor limits, but should be using the predefined objects
        optimizeWaitRequest.PreserveMetadata.add('geotag');//       shortcutting to reduce governor limits, but should be using the predefined objects
        optimizeWaitRequest.PreserveMetadata.add('orientation');//  shortcutting to reduce governor limits, but should be using the predefined objects
        optimizeWaitRequest.PreserveMetadata.add('profile');//      shortcutting to reduce governor limits, but should be using the predefined objects
        optimizeWaitRequest.SamplingScheme = '4:4:4';//             shortcutting to reduce governor limits, but should be using the predefined objects

        vNKrakenService.KrakenResponse optimizeWaitResponse = KrakenService.OptimizeWait(optimizeWaitRequest);

        if (!optimizeWaitResponse.Success) {
            System.debug('Source optimization failed, error: ' + optimizeWaitResponse.Error);

            return null;
        }

        OptimizationResult result = new OptimizationResult();

        result.FileBlob = DownloadKrakenFile(optimizeWaitResponse);
        result.FileSize = optimizeWaitResponse.KrakedSize;
        result.KeyName = keyName;

        return result;
    }

    private static OptimizationResult GetOptimizedThumbnail(
        vNOpportunityPhoto__c photo,
        blob fileBlob) {
        string keyName = vNUtilities.GetRandomString(32) + photo.SourceFile__r.Extension__c;
        vNKrakenService.OptimizeWaitRequest optimizeWaitRequest = new vNKrakenService.OptimizeWaitRequest();

        optimizeWaitRequest.AutoOrient = true;
        optimizeWaitRequest.FileBlob = fileBlob;
        optimizeWaitRequest.FileName = keyName;
        optimizeWaitRequest.Lossy = true;
        optimizeWaitRequest.Resize = new vNKrakenService.KrakenResize();
        optimizeWaitRequest.Resize.Height = 200;
        optimizeWaitRequest.Resize.Width = 200;

        vNKrakenService.KrakenResponse optimizeWaitResponse = KrakenService.OptimizeWait(optimizeWaitRequest);

        if (!optimizeWaitResponse.Success) {
            System.debug('Thumbnail optimization failed, error: ' + optimizeWaitResponse.Error);

            return null;
        }

        OptimizationResult result = new OptimizationResult();

        result.FileBlob = DownloadKrakenFile(optimizeWaitResponse);
        result.FileSize = optimizeWaitResponse.KrakedSize;
        result.KeyName = keyName;

        return result;
    }

    private class OptimizationResult {
        public blob FileBlob { get; set; }
        public integer FileSize { get; set; }
        public string KeyName { get; set; }
    }

    // @Future (Callout = true)
    private static boolean PutOptimizedBlob(
        OptimizationResult optimizationResult) {
        vNAmazonS3Service.PutObjectRequest putObjectRequest = new vNAmazonS3Service.PutObjectRequest();

        putObjectRequest.BucketName = 'Bucket';
        putObjectRequest.FileBlob = optimizationResult.FileBlob;
        putObjectRequest.KeyName = optimizationResult.KeyName;

        vNAmazonS3Service.AwsS3Response putObjectResponse = AwsService.PutObject(putObjectRequest);

        return putObjectResponse.Success;
    }
}
6

You forgot to tell the system that the Queueable class allows callouts. The documentation doesn't really call this out, but you need to use Database.AllowsCallouts, like so:

public class vNImageOptimizationJob implements Queueable, Database.AllowsCallouts {
// ...
}

This is the same limitation that Database.Batchable imposes. There's no need to make your callout methods "future" because you can do all the callouts you need in the main asynchronous thread.

I don't see anything inherently wrong with the code otherwise, but keep in mind that you're not updating the Photo record if you bail early, so they'll remain to appear "queued" and "not optimized" indefinitely. You might want a third flag to indicate failure.

1
  • That worked! I remember seeing that and adding it before without success, but thinking back on it, I might have misspelled it as .AllowCallouts without the s. Now the jobs get queued correctly, but I'm running into a separate issue with the call outs. On some of them the mutli-part form isn't composed correctly and Kraken.io can't parse the binary file which results in a 502 server error. It ends up locking the job until the system kills it. Going to have to look at my form builder. Thanks for the help! – Gup3rSuR4c Jul 18 '16 at 3:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.