celesto
AI 에이전트를 위한 안전하고 영구적인 컴퓨터
희귀 이벤트를 일관적이고 깔끔하게 처리하고, 재시도 옵션과 운영자 알림을 일등급 개념으로 제공하는 프레임워크
A framework for consistent and clean handling of rare events, with options for retry, and with operator notification as a first-class concept.
개발 도구Java · ShellGitHub @mavai-org온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A bridge between deterministic and non-deterministic worlds.
Most application code is deterministic. Given the same inputs, it produces the same outputs. But applications routinely encounter non-determinism — operations whose outcomes depend on factors outside the code's control: network availability, service responsiveness, database state, or even intentional randomness in stochastic algorithms.
Not all non-determinism implies failure. A random number generator is non-deterministic yet doesn't fail. However, in practice, the vast majority of non-deterministic operations Java developers encounter are fallible: network calls, database queries, external API invocations. These may timeout, return errors, or simply not respond. Outcome addresses this dominant case.
It provides a formal boundary where deterministic code meets fallible operations. At this boundary, uncertainty is acknowledged, observed, and converted into one of two well-defined states: Ok or Fail. From there, application code flows deterministically again — handling success or failure through normal control flow, not exception handling.
Java's exception model conflates three fundamentally different things:
Operational failures — network timeouts, service unavailability, rate limits. These are normal. They happen every day in production. They're expected, recoverable, and often transient.
Defects — null pointers, invalid arguments, misconfiguration. These are bugs. No retry will help. A human must fix the code or the configuration.
Terminal environment failures — OutOfMemoryError, StackOverflowError, NoClassDefFoundError. The JVM itself is compromised. No application-level handling is possible or advisable.
By treating the first two as "exceptions," Java's type system forces a syntactic ritual (try/catch) that looks like handling but usually isn't. Developers comply with the compiler, not with operational reality:
try {
return httpClient.send(request);
} catch (IOException e) {
log.error("Request failed", e); // Now what?
throw new RuntimeException(e); // Punt.
}
The result:
Operational failures are values. They belong in normal control flow, not exception handling.
Outcome<Response> result = boundary.call("UserApi.fetch", () -> httpClient.send(request));
return switch (result) {
case Outcome.Ok(var response) -> processResponse(response);
case Outcome.Fail(var failure) -> handleFailure(failure);
};
Defects remain exceptions. A NullPointerException should crash the operation and page an operator. No catch block will fix a bug.
Terminal errors are not handled. An OutOfMemoryError means the JVM is compromised. The framework does not catch Error—let the process die and let your infrastructure (Kubernetes, systemd, supervisors) restart it.
This isn't a new idea—it's how Rust, Go, and functional languages handle errors. Outcome brings this proven idiom to Java with full type safety and pattern matching support.
A sealed interface representing success or failure:
public sealed interface Outcome<T> permits Outcome.Ok, Outcome.Fail {
record Ok<T>(T value) implements Outcome<T> {}
record Fail<T>(Failure failure) implements Outcome<T> {}
}
Use pattern matching for exhaustive handling:
String message = switch (outcome) {
case Outcome.Ok(var user) -> "Hello, " + user.name();
case Outcome.Fail(var failure) -> "Error: " + failure.message();
};
Or use combinators for functional composition:
Outcome<Order> order = fetchUser(userId)
.flatMap(user -> fetchCart(user.cartId()))
.flatMap(cart -> createOrder(cart));
A structured failure with everything needed for reporting and policy decisions:
network:timeout, sql:connection)TRANSIENT, PERMANENT, or DEFECTRetry-After header)This isn't heavyweight ceremony—it's the information operators need to respond appropriately.
The single point where checked exceptions are translated into outcomes:
Boundary boundary = new Boundary(classifier, reporter);
Outcome<Response> result = boundary.call("HttpClient.send", () ->
httpClient.send(request) // throws IOException
);
Outcome.Fail (reported to operations)OperationalExceptionHandler at the top)The Boundary ensures checked exceptions never leak into application code while maintaining full observability.
Failures are first-class concerns, not afterthoughts buried in log files:
public interface OpReporter {
void report(Failure failure);
void reportRetryAttempt(Failure failure, int attemptNumber, Duration delay);
void reportRetryExhausted(Failure failure, int totalAttempts);
}
OpReporter implementations decide how to handle each failure based on its FailureType and their own configuration. Route to your observability stack—metrics, structured logging, Slack, Teams, or whatever your organization uses.
Retry logic belongs in policies, not scattered catch blocks:
Retrier retrier = Retrier.builder()
.policy(RetryPolicy.backoff(5, Duration.ofMillis(100), Duration.ofSeconds(10)))
.reporter(reporter)
.build();
Outcome<Response> result = retrier.execute(
() -> boundary.call("UserApi.fetch", () -> userApi.fetch(userId))
);
여기까지가 README 앞부분입니다. 나머지는 원문에서 읽을 수 있습니다. README 전문 보기 ↗