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 afterSTALE_SCENE. - Reduce dimensions, attachments, workers, or registered asset pressure after
MEMORY_LIMIT; use a realistic deadline afterTIMEOUT. - Close local state, rediscover the service, create a new client, and recapture after
CLIENT_CLOSEDorPROVIDER_DISABLED. - Select only advertised profiles/attachments after
UNSUPPORTED_CAPABILITY; treatUNSUPPORTED_PLATFORMas an operator compatibility issue. - Fix request input after
INVALID_REQUEST; fix and revalidate archive bytes afterMALFORMED_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 replacementAssetHandle, 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_FAILUREwith sanitized diagnostics and exact artifact checksums. - Treat
CANCELLEDas 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();
}
}
}