You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2025/07/21 10:44:04 UTC
(tooling-trusted-release) branch main updated: Add tips about using outcomes in difficult cases
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 <se...@miscoranda.com>
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
+@dataclasses.dataclass
+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
+@dataclasses.dataclass
+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: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org