Skip to content

Typed failures

Handle RendererException by code(), never exception text. A lifecycle race may replace later work with the failure that wins. Unwrap completion wrappers first, then map stable codes to bounded actions:

  • Retry later with backoff after QUEUE_FULL; never spin.
  • Load required chunks and recapture after MISSING_CHUNK; recapture after STALE_SCENE.
  • Reduce dimensions, attachments, workers, or registered asset pressure after MEMORY_LIMIT; use a realistic deadline after TIMEOUT.
  • Close local state, rediscover the service, create a new client, and recapture after CLIENT_CLOSED or PROVIDER_DISABLED.
  • Select only advertised profiles/attachments after UNSUPPORTED_CAPABILITY; treat UNSUPPORTED_PLATFORM as an operator compatibility issue.
  • Fix request input after INVALID_REQUEST; fix and revalidate archive bytes after MALFORMED_ASSET.
  • After MISSING_ASSET, never resubmit the same inactive hash. If the owner still has the bounded, validated ZIP bytes, register them with the current client, retain the replacement AssetHandle, recapture a scene for the current provider generation, rebuild the job with the replacement hash, and resubmit at most once. Keep the returned recovery owner until the replacement task terminates; it closes the replacement handle automatically after success, failure, or cancellation. If that recovery fails again, stop and report the lifecycle/ownership fault.
  • Report repeatable NATIVE_FAILURE with sanitized diagnostics and exact artifact checksums.
  • Treat CANCELLED as terminal when cancellation was requested.
package docs.examples;

import ke.ric.renderer.api.RendererException;

import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;

public final class FailureExample {
    public static String code(Throwable failure) {
        Throwable cause = failure;
        while ((cause instanceof CompletionException || cause instanceof ExecutionException)
                && cause.getCause() != null) cause = cause.getCause();
        return cause instanceof RendererException renderer ? renderer.code().name() : "UNEXPECTED";
    }
}
package docs.examples;

import ke.ric.renderer.api.AssetHandle;
import ke.ric.renderer.api.AssetPack;
import ke.ric.renderer.api.RenderFailureCode;
import ke.ric.renderer.api.RenderJob;
import ke.ric.renderer.api.RenderSettings;
import ke.ric.renderer.api.RenderTask;
import ke.ric.renderer.api.RendererException;
import ke.ric.renderer.api.RendererClient;
import ke.ric.renderer.api.Scene;

import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

public final class RecoveryExample {
    public enum Action {
        RETRY_LATER,
        RECAPTURE,
        REREGISTER_ASSET_RECAPTURE_RESUBMIT,
        REDUCE_REQUEST,
        REDISCOVER_PROVIDER,
        REPORT
    }

    public static Action action(RendererException failure) {
        return action(failure.code());
    }

    public static Action action(RenderFailureCode code) {
        return switch (code) {
            case QUEUE_FULL -> Action.RETRY_LATER;
            case MISSING_CHUNK, STALE_SCENE -> Action.RECAPTURE;
            case MISSING_ASSET -> Action.REREGISTER_ASSET_RECAPTURE_RESUBMIT;
            case MEMORY_LIMIT, TIMEOUT -> Action.REDUCE_REQUEST;
            case CLIENT_CLOSED, PROVIDER_DISABLED -> Action.REDISCOVER_PROVIDER;
            default -> Action.REPORT;
        };
    }

    public static boolean mayRetryMissingAsset(RenderFailureCode code, int priorAssetRetries) {
        if (priorAssetRetries < 0) throw new IllegalArgumentException("negative retry count");
        return code == RenderFailureCode.MISSING_ASSET && priorAssetRetries == 0;
    }

    @FunctionalInterface
    public interface ReplacementSceneFactory {
        Scene rebuild(String replacementAssetHash);
    }

    public static AssetRecovery recoverMissingAssetOnce(RendererClient client, byte[] validatedZip,
                                                         ReplacementSceneFactory scenes,
                                                         RenderSettings settings,
                                                         int priorAssetRetries) {
        if (!mayRetryMissingAsset(RenderFailureCode.MISSING_ASSET, priorAssetRetries)) {
            throw new IllegalStateException("missing-asset recovery is limited to one attempt");
        }
        AssetHandle replacement = client.registerAsset(
                new AssetPack("consumer-pack.zip", validatedZip));
        try {
            String replacementHash = replacement.contentHash();
            Scene replacementScene = scenes.rebuild(replacementHash);
            RenderTask replacementTask = client.submit(new RenderJob(
                    1, replacementScene, settings, List.of(replacementHash)));
            AssetRecovery recovery = new AssetRecovery(replacement, replacementTask);
            replacementTask.completion().whenComplete((result, failure) -> recovery.close());
            return recovery;
        } catch (RuntimeException failure) {
            replacement.close();
            throw failure;
        }
    }

    public static final class AssetRecovery implements AutoCloseable {
        private final AssetHandle replacement;
        private final RenderTask task;
        private final AtomicBoolean closed = new AtomicBoolean();

        private AssetRecovery(AssetHandle replacement, RenderTask task) {
            this.replacement = replacement;
            this.task = task;
        }

        public AssetHandle replacement() {
            return replacement;
        }

        public RenderTask task() {
            return task;
        }

        @Override
        public void close() {
            if (closed.compareAndSet(false, true)) replacement.close();
        }
    }
}