This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git
The following commit(s) were added to refs/heads/main by this push:
new c472cdc Add tips about using outcomes in difficult cases
c472cdc is described below
commit c472cdc23dfa2d7749c397c544e00447a9a1879f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Jul 21 11:43:43 2025 +0100
Add tips about using outcomes in difficult cases
---
docs/storage-interface.html | 30 ++++++++++++++++++++++++++++--
docs/storage-interface.md | 39 +++++++++++++++++++++++++++++++++++++++
2 files changed, 67 insertions(+), 2 deletions(-)
diff --git a/docs/storage-interface.html b/docs/storage-interface.html
index 9c9165d..02cacc3 100644
--- a/docs/storage-interface.html
+++ b/docs/storage-interface.html
@@ -23,7 +23,7 @@
<p>The <code>wafm</code> (<strong>w</strong>rite <strong>a</strong>s
<strong>f</strong>oundation <strong>m</strong>ember) object exposes
functionality which is only available to foundation members. The
<code>wafm.keys.ensure_stored_one</code> method is an example of such
functionality. The <code>wacm</code> object goes further and exposes
functionality only available to committee members.</p>
<p>In this case we decide to raise as soon as there is any error. We could
also choose instead to display a warning, ignore the error, etc.</p>
<p>The first few lines in the context session show the classic three step
approach. Here they are again with comments:</p>
-<pre><code> # 1. Request permissions
+<pre><code class="language-python"> # 1. Request permissions
wafm = write.as_foundation_member().writer_or_raise()
# 2. Use the exposed functionality
@@ -35,7 +35,7 @@
<h2>How do we add functionality to the storage interface?</h2>
<p>Add all the functionality to classes in modules in the
<code>atr/storage/writers</code> directory. Code to write public keys to
storage, for example, goes in <code>atr/storage/writers/keys.py</code>.</p>
<p>Classes in modules in the <code>atr/storage/writers</code> directory must
be named as follows:</p>
-<pre><code>class FoundationParticipant:
+<pre><code class="language-python">class FoundationParticipant:
...
class FoundationMember(FoundationParticipant):
@@ -55,6 +55,32 @@ class CommitteeMember(CommitteeParticipant):
<p>How do we represent this?</p>
<p>Outcomes are one possibility. For each key we can return
<code>OutcomeResult</code> for a success, and <code>OutcomeException</code>
when there was a Python error. The caller can then decide what to do with this
information. It might ignore the exception, raise it, or print an error message
to the user. Better yet, we can aggregate these into an <code>Outcomes</code>
list, which provides many useful methods for processing all of the outcomes
together. It can count how many exceptions [...]
<p>We do not have to return outcomes from public storage interface methods,
but these classes were designed to make the storage interface easy to use.</p>
+<h3>Outcome design patterns</h3>
+<p>One common pattern when designing outcome types is about how to handle an
<strong>exception after a success</strong>, and how to handle a <strong>warning
during success</strong>:</p>
+<ul>
+<li>An <strong>exception after a success</strong> is when an object is
processed in multiple stages, and the first few stages succeed but then
subsequently there is an exception.</li>
+<li>A <strong>warning during success</strong> is when an object is processed
in multiple stages, an exception is raised, but we determine that we can
proceed to subsequent stages as long as we keep a note of the exception.</li>
+</ul>
+<p>Both of these workflows appear incompatible with outcomes. In outcomes, we
can record <em>either</em> a successful result, <em>or</em> an exception. But
in exception after success we want to record the successes up to the exception;
and in a warning during a success we want to record the exception even though
we return a success result.</p>
+<p>The solution is similar in both cases: create a wrapper of the <em>primary
type</em> which can hold an instance of the <em>secondary type</em>.</p>
+<p>In <em>exception after a success</em> the primary type is an exception, and
the secondary type is the result which was obtained up to that exception. The
type will look like this:</p>
+<pre><code class="language-python">class AfterSuccessError(Exception):
+ def __init__(self, result_before_error: Result):
+ self.result_before_error = result_before_error
+</code></pre>
+<p>In <em>warning during success</em>, the primary type is the result, and the
secondary type is the exception raised during successful processing which we
consider a warning. This is the inverse of the above, and the types are
therefore inverted too.</p>
+<pre><code class="language-python">@dataclasses.dataclass
+class Result:
+ value: Value
+ warning: Exception | None
+</code></pre>
+<p>This could just as easily be a Pydantic class or whatever is appropriate in
the situation, as long as it can hold the warning. If the warning is generated
during an additional or side task, we can use <code>Outcome[SideValue]</code>
instead. We do this, for example, in the type representing a linked
committee:</p>
+<pre><code class="language-python">@dataclasses.dataclass
+class LinkedCommittee:
+ name: str
+ autogenerated_keys_file: Outcome[str]
+</code></pre>
+<p>In this case, if the autogenerated keys file call succeeded without an
error, the <code>Outcome</code> will be an <code>OutcomeResult[str]</code>
where the <code>str</code> represents the full path to the autogenerated
file.</p>
<h2>What makes this safe?</h2>
<p>We can always open a database session or write to the filesystem, so there
is no way to make storage access truly safe. But abstracting these operations
to a well known interface makes it more likely that we use only this way of
doing things, which we can then concentrate on getting right. This is in
contrast to writing storage access in <em>ad hoc</em> ways, some of which may
be correct and some of which may not.</p>
<p>Code relative to a permissions level is only ever exposed in the storage
interface when it is proven, at the type level and during runtime, that the
user has credentials for those permissions. Helper code remains private due to
the use of <code>__private_methods</code>, which undergo name mangling in
Python. As mentioned in the introduction, the storage interface is also the
suitable place to add audit logging, currently planned and not yet
implemented.</p>
diff --git a/docs/storage-interface.md b/docs/storage-interface.md
index a12deae..af6b9e8 100644
--- a/docs/storage-interface.md
+++ b/docs/storage-interface.md
@@ -79,6 +79,45 @@ Outcomes are one possibility. For each key we can return
`OutcomeResult` for a s
We do not have to return outcomes from public storage interface methods, but
these classes were designed to make the storage interface easy to use.
+### Outcome design patterns
+
+One common pattern when designing outcome types is about how to handle an
**exception after a success**, and how to handle a **warning during success**:
+
+* An **exception after a success** is when an object is processed in multiple
stages, and the first few stages succeed but then subsequently there is an
exception.
+* A **warning during success** is when an object is processed in multiple
stages, an exception is raised, but we determine that we can proceed to
subsequent stages as long as we keep a note of the exception.
+
+Both of these workflows appear incompatible with outcomes. In outcomes, we can
record _either_ a successful result, _or_ an exception. But in exception after
success we want to record the successes up to the exception; and in a warning
during a success we want to record the exception even though we return a
success result.
+
+The solution is similar in both cases: create a wrapper of the _primary type_
which can hold an instance of the _secondary type_.
+
+In _exception after a success_ the primary type is an exception, and the
secondary type is the result which was obtained up to that exception. The type
will look like this:
+
+```python
+class AfterSuccessError(Exception):
+ def __init__(self, result_before_error: Result):
+ self.result_before_error = result_before_error
+```
+
+In _warning during success_, the primary type is the result, and the secondary
type is the exception raised during successful processing which we consider a
warning. This is the inverse of the above, and the types are therefore inverted
too.
+
+```python
[email protected]
+class Result:
+ value: Value
+ warning: Exception | None
+```
+
+This could just as easily be a Pydantic class or whatever is appropriate in
the situation, as long as it can hold the warning. If the warning is generated
during an additional or side task, we can use `Outcome[SideValue]` instead. We
do this, for example, in the type representing a linked committee:
+
+```python
[email protected]
+class LinkedCommittee:
+ name: str
+ autogenerated_keys_file: Outcome[str]
+```
+
+In this case, if the autogenerated keys file call succeeded without an error,
the `Outcome` will be an `OutcomeResult[str]` where the `str` represents the
full path to the autogenerated file.
+
## What makes this safe?
We can always open a database session or write to the filesystem, so there is
no way to make storage access truly safe. But abstracting these operations to a
well known interface makes it more likely that we use only this way of doing
things, which we can then concentrate on getting right. This is in contrast to
writing storage access in _ad hoc_ ways, some of which may be correct and some
of which may not.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]