<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://thryveframework.github.io/Thryvals/feed.xml" rel="self" type="application/atom+xml" /><link href="https://thryveframework.github.io/Thryvals/" rel="alternate" type="text/html" /><updated>2026-11-11T23:03:03+00:00</updated><id>https://thryveframework.github.io/Thryvals/feed.xml</id><title type="html">Thryvals</title><subtitle>Write-ups from building Thryve</subtitle><entry><title type="html">How Thryve works, with two projects in one engine</title><link href="https://thryveframework.github.io/Thryvals/2026/11/12/how-thryve-works.html" rel="alternate" type="text/html" title="How Thryve works, with two projects in one engine" /><published>2026-11-12T00:00:00+00:00</published><updated>2026-11-12T00:00:00+00:00</updated><id>https://thryveframework.github.io/Thryvals/2026/11/12/how-thryve-works</id><content type="html" xml:base="https://thryveframework.github.io/Thryvals/2026/11/12/how-thryve-works.html"><![CDATA[<p>Thryve is a multithreading task engine written in Rust, with a C API for C, C++ and other languages that can call C. You describe your work as tasks, and Thryve runs them on a pool of worker threads and hands each result to the step that needs it, so your program needs no thread code of its own.</p>

<p>This write-up follows a program through Thryve from start to finish: starting an engine, adding plugins, sending plain tasks, why Thryve also has advanced tasks, observers, what happens inside the engine, and how a run ends. Each part has a small example. The last part goes through an example from the repo, where one engine runs two unrelated projects at the same time. The first release, 0.1.0, runs on Windows only for now.</p>

<h2 id="starting-an-engine">Starting an engine</h2>

<p>A program starts Thryve in one of two ways. Interactively, someone starts it from a command prompt or by double-clicking it, and it runs until a task ends the run. As a Windows service, it runs in the background under Windows’ service manager. Both start the same engine, with the same plugins and observers.</p>

<p>In interactive mode, a program loads its configuration, gets a context from it, adds its plugins and observers, and runs it:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">run</span><span class="p">::</span><span class="n">run_interactive</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="nn">cfg</span><span class="p">::</span><span class="n">Config</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="nn">error</span><span class="p">::</span><span class="n">get_err</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// thryve.toml holds the worker count, the priorities, the folders and the debugging options.</span>
    <span class="k">let</span> <span class="n">config</span> <span class="o">=</span> <span class="k">match</span> <span class="nn">Config</span><span class="p">::</span><span class="nf">load_config_from_file</span><span class="p">(</span><span class="s">"thryve.toml"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">Ok</span><span class="p">(</span><span class="n">config</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">config</span><span class="p">,</span>
        <span class="nf">Err</span><span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
            <span class="nd">eprintln!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="nf">get_err</span><span class="p">(</span><span class="o">&amp;</span><span class="n">error</span><span class="p">));</span>
            <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">};</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">ctx</span> <span class="o">=</span> <span class="nn">run_interactive</span><span class="p">::</span><span class="nf">summon_requirements</span><span class="p">(</span><span class="n">config</span><span class="p">);</span>

    <span class="c1">// The plugins and observers this engine runs.</span>
    <span class="n">ctx</span><span class="py">.plugins</span><span class="nf">.push</span><span class="p">(</span><span class="nn">Images</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.packet_handle</span><span class="p">()));</span>
    <span class="n">ctx</span><span class="py">.observers</span><span class="nf">.push</span><span class="p">(</span><span class="nn">Progress</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.packet_handle</span><span class="p">()));</span>

    <span class="k">match</span> <span class="nn">run_interactive</span><span class="p">::</span><span class="nf">run_interactive</span><span class="p">(</span><span class="n">ctx</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// The engine runs on a thread of its own, until a task ends the run.</span>
        <span class="nf">Ok</span><span class="p">(</span><span class="n">engine</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">_</span> <span class="o">=</span> <span class="n">engine</span><span class="nf">.join</span><span class="p">();</span>
        <span class="p">}</span>
        <span class="nf">Err</span><span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nd">eprintln!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="nf">get_err</span><span class="p">(</span><span class="o">&amp;</span><span class="n">error</span><span class="p">)),</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Images</code> is a plugin and <code class="language-plaintext highlighter-rouge">Progress</code> an observer; both come up below. <code class="language-plaintext highlighter-rouge">run_interactive</code> creates the run’s log folder, starts the worker threads and the observers, loads the plugins, and returns the engine’s thread. Joining that thread waits until the run ends.</p>

<p>The configuration file must set every option: a missing or unknown key is an error, reported with its line, and <code class="language-plaintext highlighter-rouge">Config::load_def_config()</code> gives the defaults instead. Each plugin and observer gets a <code class="language-plaintext highlighter-rouge">PacketHandle</code>, its way to send work to the engine. The handle refuses work until the engine exists, since nothing would read it before that: start Thryve, then send.</p>

<p>As a Windows service, the same program makes two different calls:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="p">(</span><span class="n">service</span><span class="p">,</span> <span class="k">mut</span> <span class="n">ctx</span><span class="p">)</span> <span class="o">=</span> <span class="nn">run_service</span><span class="p">::</span><span class="nf">create_service</span><span class="p">(</span><span class="n">config</span><span class="p">);</span>
<span class="n">ctx</span><span class="py">.plugins</span><span class="nf">.push</span><span class="p">(</span><span class="nn">Images</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.packet_handle</span><span class="p">()));</span>
<span class="n">ctx</span><span class="py">.observers</span><span class="nf">.push</span><span class="p">(</span><span class="nn">Progress</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">ctx</span><span class="nf">.packet_handle</span><span class="p">()));</span>

<span class="c1">// Windows' service manager starts it from here, and can stop, pause and resume it.</span>
<span class="k">if</span> <span class="k">let</span> <span class="nf">Err</span><span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="o">=</span> <span class="nn">run_service</span><span class="p">::</span><span class="nf">try_run_as_service</span><span class="p">(</span><span class="n">service</span><span class="p">,</span> <span class="n">ctx</span><span class="p">)</span> <span class="p">{</span>
    <span class="nd">eprintln!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="nf">get_err</span><span class="p">(</span><span class="o">&amp;</span><span class="n">error</span><span class="p">));</span>
    <span class="nn">std</span><span class="p">::</span><span class="nn">process</span><span class="p">::</span><span class="nf">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The configuration’s <code class="language-plaintext highlighter-rouge">[service]</code> section says whether the service accepts Stop, and Pause and Resume. Pause holds the engine’s work until Resume, and Stop shuts the engine down.</p>

<h2 id="plugins">Plugins</h2>

<p>A plugin is a self-contained part of a program: an image library, a report, a nightly backup. It has a type name, unique within an engine, an entry point and a clean-up. The entry point runs once, on a worker thread, when the engine starts, and it’s where a plugin sends its first tasks; the clean-up runs when the engine shuts down.</p>

<p>Here’s <code class="language-plaintext highlighter-rouge">Images</code>, the plugin added above. Its entry point sends one task, <code class="language-plaintext highlighter-rouge">clear_cache</code>, which the next part shows:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">sync</span><span class="p">::</span><span class="nb">Arc</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">prelude</span><span class="p">::</span><span class="o">*</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="nn">bases</span><span class="p">::</span><span class="nn">plugin</span><span class="p">::</span><span class="n">Plugin</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="n">impl_as_any</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="nn">res</span><span class="p">::</span><span class="n">TaskResult</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="nn">traits</span><span class="p">::</span><span class="n">PlgLogic</span><span class="p">;</span>

<span class="k">struct</span> <span class="n">Images</span><span class="p">;</span>

<span class="k">impl</span> <span class="n">Images</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">tx</span><span class="p">:</span> <span class="n">PacketHandle</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">Plugin</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">sender</span> <span class="o">=</span> <span class="n">tx</span><span class="nf">.clone</span><span class="p">();</span>
        <span class="c1">// The entry point: it runs once, on a worker, when the engine starts.</span>
        <span class="k">let</span> <span class="n">entry_point</span> <span class="o">=</span> <span class="k">move</span> <span class="p">||</span> <span class="k">match</span> <span class="nf">clear_cache</span><span class="p">(</span><span class="o">&amp;</span><span class="n">sender</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">Ok</span><span class="p">(())</span> <span class="k">=&gt;</span> <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Started"</span><span class="p">,</span> <span class="s">"Images is ready"</span><span class="nf">.to_string</span><span class="p">(),</span> <span class="s">"Main"</span><span class="p">),</span>
            <span class="nf">Err</span><span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Failed"</span><span class="p">,</span> <span class="nd">format!</span><span class="p">(</span><span class="s">"no cache clearing: {error}"</span><span class="p">),</span> <span class="s">"Main"</span><span class="p">),</span>
        <span class="p">};</span>
        <span class="nn">Plugin</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Images"</span><span class="p">,</span> <span class="nn">Arc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="k">Self</span><span class="p">),</span> <span class="n">tx</span><span class="p">,</span> <span class="n">entry_point</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"a valid plugin type"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">PlgLogic</span> <span class="k">for</span> <span class="n">Images</span> <span class="p">{</span>
    <span class="c1">// Runs when the engine shuts down, to let go of what the plugin holds.</span>
    <span class="k">fn</span> <span class="nf">clean_up</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="nf">Ok</span><span class="p">(())</span>
    <span class="p">}</span>

    <span class="nd">impl_as_any!</span><span class="p">(</span><span class="n">Images</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The entry point is a task itself, so it returns a task result, the subject of the next part. After it, a plugin’s other functions can send tasks at any time through its <code class="language-plaintext highlighter-rouge">PacketHandle</code>, whether they’re called from its own tasks or from the program around it.</p>

<p>Because a plugin is self-contained, a program can take one, or a bundle of plugins that work together, and use it without changing its code: the program adds it to the context, and the plugin does its part. A second plugin is one more <code class="language-plaintext highlighter-rouge">push</code>.</p>

<h2 id="plain-tasks">Plain tasks</h2>

<p>The simplest unit of work is a plain task: a name, a priority and a closure. The closure returns a <code class="language-plaintext highlighter-rouge">TaskResult</code>, which has three parts: a result type, a message, and the log file the message goes to.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="nn">packets</span><span class="p">::</span><span class="nn">task</span><span class="p">::</span><span class="n">Task</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">clear_cache</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">task</span> <span class="o">=</span> <span class="nn">Task</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"ClearCache"</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">removed</span> <span class="o">=</span> <span class="nf">remove_cached_files</span><span class="p">();</span>
        <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Done"</span><span class="p">,</span> <span class="nd">format!</span><span class="p">(</span><span class="s">"removed {removed} cached files"</span><span class="p">),</span> <span class="s">"Main"</span><span class="p">)</span>
    <span class="p">})</span><span class="o">?</span><span class="p">;</span>
    <span class="n">handle</span><span class="nf">.send_task</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li><strong>The result type</strong> is a word you pick, such as Done, Skipped or Failed. Two words are special: <code class="language-plaintext highlighter-rouge">Fin</code> ends the run, and <code class="language-plaintext highlighter-rouge">FinErr</code> ends it after an error.</li>
  <li><strong>The message</strong> goes to the log file named last. <code class="language-plaintext highlighter-rouge">Main</code> is Thryve’s own; a program adds others to <code class="language-plaintext highlighter-rouge">ctx.logging_file_names</code> before it starts. Each run’s logs go in a folder of their own.</li>
  <li><strong>The priority</strong> decides which waiting task a free worker takes first: higher goes first. More on that in “Inside the engine”.</li>
</ul>

<p>A plain task can also start the next one. <code class="language-plaintext highlighter-rouge">chain</code> puts a task on the result, and the engine sends it once this one is done:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">resize_then_save</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">save</span> <span class="o">=</span> <span class="nn">Task</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Save"</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
        <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Done"</span><span class="p">,</span> <span class="s">"thumbnail saved"</span><span class="nf">.to_string</span><span class="p">(),</span> <span class="s">"Main"</span><span class="p">)</span>
    <span class="p">})</span><span class="o">?</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">resize</span> <span class="o">=</span> <span class="nn">Task</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Resize"</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="k">move</span> <span class="p">||</span> <span class="p">{</span>
        <span class="c1">// `chain` puts Save on Resize's result: the engine sends Save once Resize is done.</span>
        <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Done"</span><span class="p">,</span> <span class="s">"photo resized"</span><span class="nf">.to_string</span><span class="p">(),</span> <span class="s">"Main"</span><span class="p">)</span><span class="nf">.chain</span><span class="p">(</span><span class="n">save</span><span class="p">)</span>
    <span class="p">})</span><span class="o">?</span><span class="p">;</span>
    <span class="n">handle</span><span class="nf">.send_task</span><span class="p">(</span><span class="n">resize</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Plain tasks suit fire-and-forget work: clearing a cache, writing a file, sending a message.</p>

<h2 id="where-plain-tasks-fall-short">Where plain tasks fall short</h2>

<p>Plain tasks work well until one step needs what another one produced. Take an image library that hashes a file, then loads the image with that digest. Three problems show up.</p>

<p><strong>1- Nothing is handed on:</strong> A plain task’s result is a word and a log line, not a value. To give the digest to the next step, Hash has to build that step itself, with the digest inside it, and chain it:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">hash_then_load_plain</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">,</span> <span class="n">path</span><span class="p">:</span> <span class="n">PathBuf</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">hash</span> <span class="o">=</span> <span class="nn">Task</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Hash"</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="k">move</span> <span class="p">||</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">digest</span> <span class="o">=</span> <span class="k">match</span> <span class="nf">hash_file</span><span class="p">(</span><span class="o">&amp;</span><span class="n">path</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">Ok</span><span class="p">(</span><span class="n">digest</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="n">digest</span><span class="p">,</span>
            <span class="c1">// A failure ends here, as a line in the log.</span>
            <span class="nf">Err</span><span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
                <span class="k">return</span> <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Failed"</span><span class="p">,</span> <span class="nd">format!</span><span class="p">(</span><span class="s">"could not hash: {error}"</span><span class="p">),</span> <span class="s">"Main"</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">};</span>
        <span class="c1">// Hash has to build the next step itself, with the digest inside it.</span>
        <span class="k">let</span> <span class="n">load</span> <span class="o">=</span> <span class="nn">Task</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"LoadImg"</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="k">move</span> <span class="p">||</span> <span class="k">match</span> <span class="nf">load_image</span><span class="p">(</span><span class="o">&amp;</span><span class="n">path</span><span class="p">,</span> <span class="n">digest</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">Ok</span><span class="p">(</span><span class="n">image</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Done"</span><span class="p">,</span> <span class="nd">format!</span><span class="p">(</span><span class="s">"loaded {} bytes"</span><span class="p">,</span> <span class="n">image</span><span class="nf">.len</span><span class="p">()),</span> <span class="s">"Main"</span><span class="p">),</span>
            <span class="nf">Err</span><span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Failed"</span><span class="p">,</span> <span class="nd">format!</span><span class="p">(</span><span class="s">"could not load: {error}"</span><span class="p">),</span> <span class="s">"Main"</span><span class="p">),</span>
        <span class="p">})</span>
        <span class="nf">.expect</span><span class="p">(</span><span class="s">"a valid task name"</span><span class="p">);</span>
        <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Done"</span><span class="p">,</span> <span class="s">"hashed"</span><span class="nf">.to_string</span><span class="p">(),</span> <span class="s">"Main"</span><span class="p">)</span><span class="nf">.chain</span><span class="p">(</span><span class="n">load</span><span class="p">)</span>
    <span class="p">})</span><span class="o">?</span><span class="p">;</span>
    <span class="n">handle</span><span class="nf">.send_task</span><span class="p">(</span><span class="n">hash</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Hash now names LoadImg. Anyone who wants to hash a file and then do something else, such as upload it or compare it with another, has to change Hash.</p>

<p><strong>2- No waiting for several tasks:</strong> Say a photo is cut into 8 parts, each resized by a task of its own, and Stitch should run once all 8 are done. Plain tasks can’t wait for each other, so you count the finished parts yourself, in shared state, and the last part to finish starts Stitch:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">resize_parts_plain</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">,</span> <span class="n">parts</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Part</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">total</span> <span class="o">=</span> <span class="n">parts</span><span class="nf">.len</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">finished</span> <span class="o">=</span> <span class="nn">Arc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="nn">AtomicUsize</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>
    <span class="k">for</span> <span class="n">part</span> <span class="k">in</span> <span class="n">parts</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">finished</span> <span class="o">=</span> <span class="nn">Arc</span><span class="p">::</span><span class="nf">clone</span><span class="p">(</span><span class="o">&amp;</span><span class="n">finished</span><span class="p">);</span>
        <span class="k">let</span> <span class="n">task</span> <span class="o">=</span> <span class="nn">Task</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"ResizePart"</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="k">move</span> <span class="p">||</span> <span class="p">{</span>
            <span class="nf">resize_part</span><span class="p">(</span><span class="n">part</span><span class="p">);</span>
            <span class="k">let</span> <span class="n">result</span> <span class="o">=</span> <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Done"</span><span class="p">,</span> <span class="s">"part resized"</span><span class="nf">.to_string</span><span class="p">(),</span> <span class="s">"Main"</span><span class="p">);</span>
            <span class="c1">// Shared state, counted by hand: the last part to finish starts the next step.</span>
            <span class="k">if</span> <span class="n">finished</span><span class="nf">.fetch_add</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nn">Ordering</span><span class="p">::</span><span class="n">SeqCst</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">==</span> <span class="n">total</span> <span class="p">{</span>
                <span class="n">result</span><span class="nf">.chain</span><span class="p">(</span><span class="nf">stitch_task</span><span class="p">())</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="n">result</span>
            <span class="p">}</span>
        <span class="p">})</span><span class="o">?</span><span class="p">;</span>
        <span class="n">handle</span><span class="nf">.send_task</span><span class="p">(</span><span class="n">task</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That’s thread code again, and it isn’t finished: the resized parts still need a place to meet, behind a lock, before Stitch can use them.</p>

<p><strong>3- Errors stop at the log:</strong> When Hash fails, its result says so in a log file, and that’s all. The step after it never hears about it, unless Hash also builds a different next step for each failure.</p>

<p>These three problems are why Thryve also has advanced tasks.</p>

<h2 id="advanced-tasks">Advanced tasks</h2>

<p>An advanced task (<code class="language-plaintext highlighter-rouge">AdvTask</code>, called a typed task in the API docs) returns a <code class="language-plaintext highlighter-rouge">Result</code>: a value, or an error of its own type. Sending one gives back a handle, and <code class="language-plaintext highlighter-rouge">then</code> on the handle declares the step that follows. When the task finishes, the engine hands its value or its error to that step, as a <code class="language-plaintext highlighter-rouge">Handoff</code>.</p>

<p>That changes who decides what comes next. A plugin offers its advanced tasks as functions that send a task and return its handle, and the program that uses the plugin declares what follows:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">prelude</span><span class="p">::</span><span class="o">*</span><span class="p">;</span>

<span class="c1">// In the Hasher plugin: it sends a Hash task and hands back the task's handle.</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">hash</span><span class="p">(</span>
    <span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">,</span>
    <span class="n">path</span><span class="p">:</span> <span class="n">PathBuf</span><span class="p">,</span>
<span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="n">AdvTaskHandle</span><span class="o">&lt;</span><span class="nb">u64</span><span class="p">,</span> <span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="n">Error</span><span class="o">&gt;</span><span class="p">,</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">task</span> <span class="o">=</span> <span class="nn">AdvTask</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Hash"</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="k">move</span> <span class="p">||</span> <span class="nf">hash_file</span><span class="p">(</span><span class="o">&amp;</span><span class="n">path</span><span class="p">))</span><span class="o">?</span><span class="p">;</span>
    <span class="n">handle</span><span class="nf">.send_adv_task</span><span class="p">(</span><span class="n">task</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// In the program that uses both plugins: the step after Hash is decided here.</span>
<span class="k">fn</span> <span class="nf">open_photo</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">,</span> <span class="n">path</span><span class="p">:</span> <span class="n">PathBuf</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">load_path</span> <span class="o">=</span> <span class="n">path</span><span class="nf">.clone</span><span class="p">();</span>
    <span class="nn">hasher</span><span class="p">::</span><span class="nf">hash</span><span class="p">(</span><span class="n">handle</span><span class="p">,</span> <span class="n">path</span><span class="p">)</span><span class="o">?</span><span class="nf">.then</span><span class="p">(</span><span class="s">"LoadImg"</span><span class="p">,</span> <span class="k">move</span> <span class="p">|</span><span class="n">handoff</span><span class="p">|</span> <span class="k">match</span> <span class="n">handoff</span> <span class="p">{</span>
        <span class="nn">Handoff</span><span class="p">::</span><span class="nf">Value</span><span class="p">(</span><span class="n">digest</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nn">images</span><span class="p">::</span><span class="nf">load</span><span class="p">(</span><span class="o">&amp;</span><span class="n">load_path</span><span class="p">,</span> <span class="n">digest</span><span class="p">),</span>
        <span class="c1">// Hash's own error, handed on to whoever follows LoadImg.</span>
        <span class="nn">Handoff</span><span class="p">::</span><span class="nf">Error</span><span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nf">Err</span><span class="p">(</span><span class="n">error</span><span class="p">),</span>
    <span class="p">})</span><span class="o">?</span><span class="p">;</span>
    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Hash doesn’t know what follows it. Another program can follow it with an upload or a comparison, and the Hasher plugin stays as it is. The error travels too: if the file can’t be read, LoadImg gets Hash’s own error, with its type, and passes it on.</p>

<ul>
  <li><strong>The result waits for its follow-up:</strong> The engine keeps a task’s result until a step is declared after it, so <code class="language-plaintext highlighter-rouge">then</code> works before or after the task finishes.</li>
  <li><strong>Steps chain:</strong> <code class="language-plaintext highlighter-rouge">then</code> returns the follow-up’s handle, so LoadImg can be followed by Save, and Save by Upload.</li>
  <li><strong>One follow-up per task:</strong> <code class="language-plaintext highlighter-rouge">then</code> uses the handle up. To give one result to several steps, there’s fan-out, in the next part.</li>
  <li><strong>Errors aren’t lost:</strong> A handle dropped without a follow-up lets its result go: a value is dropped, and an error is written to <code class="language-plaintext highlighter-rouge">Error.log</code>.</li>
  <li><strong>Priority:</strong> A follow-up inherits its task’s priority, and <code class="language-plaintext highlighter-rouge">then_with_priority</code> gives it one of its own.</li>
</ul>

<p>Plain tasks are still there, for work that hands nothing on.</p>

<h2 id="splitting-joining-and-fanning-out">Splitting, joining and fanning out</h2>

<p><code class="language-plaintext highlighter-rouge">then</code> links one step to the next. Three more shapes cover the rest: one job cut over several tasks, one step after several, and several steps after one.</p>

<p><strong>Split and join:</strong> <code class="language-plaintext highlighter-rouge">split_into</code> cuts a list into pieces and sends one advanced task per piece, each running the same function on its share. It returns their handles, in order, and <code class="language-plaintext highlighter-rouge">join</code> declares one step that runs once all of them are done, with every value in that order. Here’s the photo from before, without the counter:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">resize_photo</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">,</span> <span class="n">rows</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="n">Row</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="c1">// 8 tasks, each resizing its share of the rows; then one Stitch with all their results.</span>
    <span class="n">handle</span>
        <span class="nf">.split_into</span><span class="p">(</span><span class="n">rows</span><span class="p">,</span> <span class="s">"ResizeRows"</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="p">|</span><span class="n">chunk</span><span class="p">,</span> <span class="n">_start</span><span class="p">|</span> <span class="nf">resize_rows</span><span class="p">(</span><span class="n">chunk</span><span class="p">))</span><span class="o">?</span>
        <span class="nf">.join</span><span class="p">(</span><span class="s">"Stitch"</span><span class="p">,</span> <span class="p">|</span><span class="n">joined</span><span class="p">|</span> <span class="k">match</span> <span class="n">joined</span> <span class="p">{</span>
            <span class="c1">// Every piece's rows, in order.</span>
            <span class="nn">JoinHandoff</span><span class="p">::</span><span class="n">Filled</span> <span class="p">{</span> <span class="n">values</span> <span class="p">}</span> <span class="k">=&gt;</span> <span class="nf">Ok</span><span class="p">(</span><span class="nf">stitch</span><span class="p">(</span><span class="n">values</span><span class="p">)),</span>
            <span class="c1">// The first piece that failed, with its own error.</span>
            <span class="nn">JoinHandoff</span><span class="p">::</span><span class="n">Error</span> <span class="p">{</span> <span class="n">error</span><span class="p">,</span> <span class="o">..</span> <span class="p">}</span> <span class="k">=&gt;</span> <span class="nf">Err</span><span class="p">(</span><span class="n">error</span><span class="p">),</span>
        <span class="p">})</span><span class="o">?</span><span class="p">;</span>
    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A join fails fast: when one piece fails, Stitch runs at once with that error, and the pieces that haven’t started are cancelled. <code class="language-plaintext highlighter-rouge">join</code> works on any list of handles of one type, not only those from <code class="language-plaintext highlighter-rouge">split_into</code>, and a join can itself be one of the tasks of another join.</p>

<p><strong>Fan-out:</strong> Several steps can share one result. <code class="language-plaintext highlighter-rouge">fan_out</code> turns a handle into a group, <code class="language-plaintext highlighter-rouge">then</code> on the group adds each step, and <code class="language-plaintext highlighter-rouge">send_fanout</code> sends them:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">photo_opened</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">load</span> <span class="o">=</span> <span class="nn">AdvTask</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"LoadImg"</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="p">||</span> <span class="nn">std</span><span class="p">::</span><span class="nn">fs</span><span class="p">::</span><span class="nf">read</span><span class="p">(</span><span class="s">"photo.png"</span><span class="p">))</span><span class="o">?</span><span class="p">;</span>

    <span class="c1">// Thumbnail and Compress both read LoadImg's bytes: one result, shared.</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">group</span> <span class="o">=</span> <span class="n">handle</span><span class="nf">.send_adv_task</span><span class="p">(</span><span class="n">load</span><span class="p">)</span><span class="o">?</span><span class="nf">.fan_out</span><span class="p">();</span>
    <span class="n">group</span><span class="nf">.then</span><span class="p">(</span><span class="s">"Thumbnail"</span><span class="p">,</span> <span class="p">|</span><span class="n">pixels</span><span class="p">|</span> <span class="k">match</span> <span class="o">&amp;*</span><span class="n">pixels</span> <span class="p">{</span>
        <span class="nn">Handoff</span><span class="p">::</span><span class="nf">Value</span><span class="p">(</span><span class="n">bytes</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">bytes</span><span class="nf">.len</span><span class="p">()</span> <span class="o">/</span> <span class="mi">4</span><span class="p">),</span>
        <span class="nn">Handoff</span><span class="p">::</span><span class="nf">Error</span><span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nf">Err</span><span class="p">(</span><span class="n">error</span><span class="nf">.clone</span><span class="p">()),</span>
    <span class="p">})</span><span class="o">?</span><span class="p">;</span>
    <span class="n">group</span><span class="nf">.then</span><span class="p">(</span><span class="s">"Compress"</span><span class="p">,</span> <span class="p">|</span><span class="n">pixels</span><span class="p">|</span> <span class="k">match</span> <span class="o">&amp;*</span><span class="n">pixels</span> <span class="p">{</span>
        <span class="nn">Handoff</span><span class="p">::</span><span class="nf">Value</span><span class="p">(</span><span class="n">bytes</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">bytes</span><span class="nf">.iter</span><span class="p">()</span><span class="nf">.map</span><span class="p">(|</span><span class="n">byte</span><span class="p">|</span> <span class="nn">u64</span><span class="p">::</span><span class="nf">from</span><span class="p">(</span><span class="o">*</span><span class="n">byte</span><span class="p">))</span><span class="py">.sum</span><span class="p">::</span><span class="o">&lt;</span><span class="nb">u64</span><span class="o">&gt;</span><span class="p">()),</span>
        <span class="nn">Handoff</span><span class="p">::</span><span class="nf">Error</span><span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="nf">Err</span><span class="p">(</span><span class="n">error</span><span class="nf">.clone</span><span class="p">()),</span>
    <span class="p">})</span><span class="o">?</span><span class="p">;</span>
    <span class="n">group</span><span class="nf">.send_fanout</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>
    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Every step in the group reads the same result, not a copy, and the steps can run at the same time on different workers.</p>

<p><strong>Splitting has a cost:</strong> Every piece is a task, with its own trip through the queue, and every join adds one more. Splitting finer than the work needs adds that cost without making anything faster, so aim for enough pieces to keep the workers busy, each big enough to be worth a task.</p>

<h2 id="forwarding-and-cancelling">Forwarding and cancelling</h2>

<p><strong>Forwarding:</strong> Sometimes a task only knows its last step once it runs. A task made with <code class="language-plaintext highlighter-rouge">AdvTask::forward</code> creates that step while it runs and returns its handle, and whoever follows the task gets that step’s result:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">prepare_photo</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">sender</span> <span class="o">=</span> <span class="n">handle</span><span class="nf">.clone</span><span class="p">();</span>
    <span class="c1">// Which step comes next is only known once the photo's size is.</span>
    <span class="k">let</span> <span class="n">prepare</span> <span class="o">=</span> <span class="nn">AdvTask</span><span class="p">::</span><span class="nf">forward</span><span class="p">(</span><span class="s">"Prepare"</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="k">move</span> <span class="p">||</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">size</span> <span class="o">=</span> <span class="nn">std</span><span class="p">::</span><span class="nn">fs</span><span class="p">::</span><span class="nf">metadata</span><span class="p">(</span><span class="s">"photo.png"</span><span class="p">)</span><span class="o">?</span><span class="nf">.len</span><span class="p">();</span>
        <span class="k">let</span> <span class="n">next</span> <span class="o">=</span> <span class="k">if</span> <span class="n">size</span> <span class="o">&gt;</span> <span class="mi">1_000_000</span> <span class="p">{</span>
            <span class="nn">AdvTask</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Shrink"</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="k">move</span> <span class="p">||</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">size</span> <span class="o">/</span> <span class="mi">4</span><span class="p">))</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nn">AdvTask</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Keep"</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="k">move</span> <span class="p">||</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">size</span><span class="p">))</span>
        <span class="p">}</span>
        <span class="nf">.map_err</span><span class="p">(</span><span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="nn">Error</span><span class="p">::</span><span class="n">other</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>

        <span class="n">sender</span><span class="nf">.send_adv_task</span><span class="p">(</span><span class="n">next</span><span class="p">)</span><span class="nf">.map_err</span><span class="p">(</span><span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="nn">Error</span><span class="p">::</span><span class="n">other</span><span class="p">)</span>
    <span class="p">})</span><span class="o">?</span><span class="p">;</span>

    <span class="c1">// Whoever follows Prepare gets the value of Shrink or Keep.</span>
    <span class="n">handle</span><span class="nf">.send_adv_task</span><span class="p">(</span><span class="n">prepare</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>No worker waits for the forwarded step: Prepare finishes as soon as its closure returns, and the engine makes Prepare’s follow-up wait instead. When the steps are known up front, <code class="language-plaintext highlighter-rouge">then</code> is the simpler way.</p>

<p><strong>Cancelling:</strong> <code class="language-plaintext highlighter-rouge">cancel</code> on a handle stops a task that hasn’t started: the worker that takes it skips it.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">photo_closed</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">thumbnail</span> <span class="o">=</span> <span class="nn">AdvTask</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Thumbnail"</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="p">||</span> <span class="nf">make_thumbnail</span><span class="p">(</span><span class="nn">Path</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"photo.png"</span><span class="p">)))</span><span class="o">?</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">thumbnail</span> <span class="o">=</span> <span class="n">handle</span><span class="nf">.send_adv_task</span><span class="p">(</span><span class="n">thumbnail</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>

    <span class="c1">// The photo was closed before its thumbnail was made: it isn't needed any more.</span>
    <span class="n">thumbnail</span><span class="nf">.cancel</span><span class="p">();</span>
    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li><strong>The work only it needed stops too:</strong> If Save follows LoadImg, which follows Hash, cancelling Save also cancels LoadImg and Hash, as long as they haven’t started. Cancelling a join cancels the tasks it waits for, and <code class="language-plaintext highlighter-rouge">cancel_all</code> cancels a whole list, such as the pieces from <code class="language-plaintext highlighter-rouge">split_into</code>.</li>
  <li><strong>A running task finishes:</strong> Its result is let go: a value is dropped, and an error is still written to <code class="language-plaintext highlighter-rouge">Error.log</code>.</li>
  <li><strong>Quiet:</strong> Cancelling never fails and writes nothing to the logs.</li>
</ul>

<h2 id="observers">Observers</h2>

<p>An observer runs on a thread of its own for the whole run, and receives notifications: messages addressed to it by its type name. A plugin, a task, the program or another observer can send one, from anywhere that holds a <code class="language-plaintext highlighter-rouge">PacketHandle</code>, and a notification can carry any type of yours:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">sync</span><span class="p">::</span><span class="nb">Arc</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">prelude</span><span class="p">::</span><span class="o">*</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="nn">bases</span><span class="p">::</span><span class="nn">notif</span><span class="p">::</span><span class="n">Notif</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="nn">bases</span><span class="p">::</span><span class="nn">observer</span><span class="p">::</span><span class="n">Observer</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="n">impl_as_any</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="nn">jobs</span><span class="p">::</span><span class="n">ObsJob</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">thryve</span><span class="p">::</span><span class="nn">utils</span><span class="p">::</span><span class="nn">traits</span><span class="p">::{</span><span class="n">NotifLogic</span><span class="p">,</span> <span class="n">ObsLogic</span><span class="p">,</span> <span class="n">Queue</span><span class="p">};</span>

<span class="c1">// What a notification carries: any type of yours.</span>
<span class="k">struct</span> <span class="n">PhotoDone</span> <span class="p">{</span>
    <span class="n">name</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">NotifLogic</span> <span class="k">for</span> <span class="n">PhotoDone</span> <span class="p">{</span>
    <span class="nd">impl_as_any!</span><span class="p">(</span><span class="n">PhotoDone</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// Sent from anywhere that holds a PacketHandle: a plugin, a task, another observer.</span>
<span class="k">fn</span> <span class="nf">photo_done</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">notif</span> <span class="o">=</span> <span class="nn">Notif</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"PhotoDone"</span><span class="p">,</span> <span class="s">"Progress"</span><span class="p">,</span> <span class="nn">Box</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">PhotoDone</span> <span class="p">{</span> <span class="n">name</span> <span class="p">}))</span><span class="o">?</span><span class="p">;</span>
    <span class="n">handle</span><span class="nf">.send_notification</span><span class="p">(</span><span class="n">notif</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The engine passes each notification to a sorter thread, which puts it in its observer’s queue. <code class="language-plaintext highlighter-rouge">Progress</code>, the observer added at the start, takes them one at a time:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">Progress</span><span class="p">;</span>

<span class="k">impl</span> <span class="n">Progress</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">new</span><span class="p">(</span><span class="n">tx</span><span class="p">:</span> <span class="n">PacketHandle</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="n">Observer</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">job</span> <span class="o">=</span> <span class="nn">ObsJob</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="k">move</span> <span class="p">|</span><span class="n">observer</span><span class="p">:</span> <span class="nb">Arc</span><span class="o">&lt;</span><span class="n">Observer</span><span class="o">&gt;</span><span class="p">|</span> <span class="p">{</span>
            <span class="c1">// The observer's own state: only its thread touches it, so no lock.</span>
            <span class="k">let</span> <span class="k">mut</span> <span class="n">done</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
            <span class="c1">// `pop` waits for the next notification, and returns `None` once the engine stops.</span>
            <span class="k">while</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">notif</span><span class="p">)</span> <span class="o">=</span> <span class="n">observer</span><span class="py">.obs_queue</span><span class="nf">.pop</span><span class="p">()</span> <span class="p">{</span>
                <span class="k">if</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">photo</span><span class="p">)</span> <span class="o">=</span> <span class="n">notif</span><span class="py">.notif_core</span><span class="nf">.as_any</span><span class="p">()</span><span class="py">.downcast_ref</span><span class="p">::</span><span class="o">&lt;</span><span class="n">PhotoDone</span><span class="o">&gt;</span><span class="p">()</span> <span class="p">{</span>
                    <span class="n">done</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
                    <span class="nd">println!</span><span class="p">(</span><span class="s">"{done} photos done, the last one {}"</span><span class="p">,</span> <span class="n">photo</span><span class="py">.name</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">});</span>
        <span class="nn">Observer</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Progress"</span><span class="p">,</span> <span class="nn">Arc</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="k">Self</span><span class="p">),</span> <span class="n">job</span><span class="p">,</span> <span class="n">tx</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"a valid observer type"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">impl</span> <span class="n">ObsLogic</span> <span class="k">for</span> <span class="n">Progress</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">clean_up</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="p">,</span> <span class="n">_observer</span><span class="p">:</span> <span class="nb">Arc</span><span class="o">&lt;</span><span class="n">Observer</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{}</span>

    <span class="nd">impl_as_any!</span><span class="p">(</span><span class="n">Progress</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A task runs once, on whichever worker is free, and is done. An observer stays for the whole run and sees its notifications one at a time, so it can keep state between them, like the count above, without a lock. That makes observers the place for:</p>

<ul>
  <li><strong>Showing what happens:</strong> printing, a window, a status page. In the example below, Console prints the reports.</li>
  <li><strong>Keeping state across many events:</strong> counting, collecting results that arrive from different tasks, noticing when the last one is in.</li>
  <li><strong>Logging:</strong> Thryve’s own logger is an observer, and every result’s message reaches it as a notification.</li>
  <li><strong>Reacting:</strong> an observer holds a <code class="language-plaintext highlighter-rouge">PacketHandle</code>, so it can send tasks when something happens. Console ends the run with a <code class="language-plaintext highlighter-rouge">Fin</code> task once every report is in.</li>
</ul>

<p>An observer’s notifications wait while it’s busy, so long work belongs in tasks, which the observer can send.</p>

<h2 id="inside-the-engine">Inside the engine</h2>

<p>Everything a program sends goes through the engine, which owns the worker threads. The drawing follows the work from the moment it’s sent:</p>

<p><img src="/Thryvals/assets/engine.svg" alt="Tasks, results and notifications all go through the engine" /></p>

<ol>
  <li><strong>Mailboxes:</strong> Every task, notification and result lands in one of four mailboxes, which only the engine reads, in the same order each turn: Thryve’s own status messages first, then tasks, notifications and results.</li>
  <li><strong>The queue:</strong> Tasks wait in a priority queue, and a free worker takes the one with the highest priority.</li>
  <li><strong>The workers:</strong> run the tasks, skip the cancelled ones, and report each result back to the engine.</li>
  <li><strong>The wait tables:</strong> A follow-up, a join or a fan-out step waits there until its inputs arrive. Then it joins the queue like any other task.</li>
  <li><strong>The observers:</strong> Notifications go to a sorter thread, which puts each in its observer’s queue.</li>
</ol>

<p>Priorities run from 0 up to <code class="language-plaintext highlighter-rouge">max_priority</code> in the configuration, which is 4 by default and 14 at most, and higher goes first. Two rules shape that order:</p>

<ul>
  <li><strong>Realtime.</strong> The top levels, from 80% of <code class="language-plaintext highlighter-rouge">max_priority</code> rounded down, are realtime: those tasks always go before the others. With the defaults, that’s priorities 3 and 4.</li>
  <li><strong>Ageing.</strong> A waiting task gains one level every <code class="language-plaintext highlighter-rouge">aging_interval</code> seconds, 3 by default, so low-priority work still gets its turn on a busy engine. Ageing never makes a task realtime.</li>
</ul>

<p>A follow-up inherits its task’s priority and a join takes the highest among its tasks, but neither inherits realtime: with the defaults, an inherited priority stops at 2. The configuration sets the rest, such as the number of workers:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[threads]</span>
<span class="py">worker_count</span> <span class="p">=</span> <span class="mi">4</span>

<span class="nn">[tasks]</span>
<span class="py">max_priority</span> <span class="p">=</span> <span class="mi">4</span>
<span class="py">aging_interval</span> <span class="p">=</span> <span class="mi">3</span>
</code></pre></div></div>

<h2 id="how-a-run-ends">How a run ends</h2>

<p>A run ends when a task’s result type is <code class="language-plaintext highlighter-rouge">Fin</code>, or <code class="language-plaintext highlighter-rouge">FinErr</code> after an error. Any plugin, observer or task can send that task; in the example below, Console does, once every report is printed:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">end_the_run</span><span class="p">(</span><span class="n">handle</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">ThryveError</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">fin</span> <span class="o">=</span> <span class="nn">Task</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Shutdown"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="p">||</span> <span class="p">{</span>
        <span class="nn">TaskResult</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"Fin"</span><span class="p">,</span> <span class="s">"every photo is done"</span><span class="nf">.to_string</span><span class="p">(),</span> <span class="s">"Main"</span><span class="p">)</span>
    <span class="p">})</span><span class="o">?</span><span class="p">;</span>
    <span class="n">handle</span><span class="nf">.send_task</span><span class="p">(</span><span class="n">fin</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li><strong>Shutting down:</strong> The engine stops taking new work and waits for the tasks still running, up to <code class="language-plaintext highlighter-rouge">shutdown_grace_period</code> (10 seconds by default). Then the plugins’ and observers’ clean-ups run, and in interactive mode <code class="language-plaintext highlighter-rouge">engine.join()</code> returns. A task still running after the grace period is named in <code class="language-plaintext highlighter-rouge">Error.log</code> and left behind, and the plugins aren’t cleaned up.</li>
  <li><strong>The logs:</strong> Each run’s log folder holds the <code class="language-plaintext highlighter-rouge">Main</code> log, the logs the program registered, and <code class="language-plaintext highlighter-rouge">Error.log</code>: errors no step took, work that never ran, a start that failed, and warnings from graph mode.</li>
  <li><strong>Panics:</strong> A panic in any task ends the whole process: Thryve writes it to <code class="language-plaintext highlighter-rouge">Error.log</code> and exits with code 1.</li>
  <li><strong>The task graph:</strong> With debugging and <code class="language-plaintext highlighter-rouge">visual_graph</code> on, Thryve records which line of code sent each task, and which task it came from. At shutdown it exports that graph and opens it in Topology, a program on Thryve’s release page that goes next to your program’s <code class="language-plaintext highlighter-rouge">.exe</code>.</li>
</ul>

<h2 id="the-example">The example</h2>

<p>The repo’s examples put these parts together in one program, <code class="language-plaintext highlighter-rouge">projects</code>. It runs two unrelated projects in one engine: a report on a web server’s access log, and a count of the primes below 10,000,000. Both run at the same time, on the same 4 worker threads, and neither project names the other.</p>

<p>Each project is a plugin, and each one ends by sending its report to Console, an observer, which prints them both:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Access log report
Note: The following numbers are calculations from the log file and are not metrics related to Thryve itself.
  requests: 10000
  data sent in responses: 131.9 MB
  lines skipped (not requests): 3

Status codes
  200    9195   92.0%
  301      50    0.5%
  304     384    3.8%
  404     295    3.0%
  500      76    0.8%

Busiest pages
  /                       1784 requests     23 ms on average
  /api/tasks              1192 requests    195 ms on average
  /docs                    978 requests     29 ms on average
  /docs/getting-started    809 requests     34 ms on average
  /api/search              768 requests    740 ms on average

Alerts
  /api/search           slow: 740 ms on average
  /api/tasks            server errors: 6.4% of its requests

Primes report
Note: The following numbers are calculations about the numbers below 10,000,000 and are not metrics related to Thryve itself.
  primes: 664579
  twin-prime pairs: 58980
  largest gap: 154, after 4652353
</code></pre></div></div>

<p><strong>These numbers are the example’s own output, not measurements of Thryve</strong>, and each report says so under its title. The log report counts a sample log of a made-up web server: 10,000 requests, written by a script in the repo. The prime counts can be checked: <a href="https://oeis.org/A006880">664,579 primes</a> and <a href="https://oeis.org/A007508">58,980 twin-prime pairs</a> below 10,000,000.</p>

<h2 id="how-a-project-declares-its-work">How a project declares its work</h2>

<p>Each project declares all of its steps once, when the engine starts, and the engine runs each step as soon as its input is ready. The log report’s plugin, LogReader, reads the log in its entry point and hands the lines to <code class="language-plaintext highlighter-rouge">declare_report</code>, which is the whole report:</p>

<p><img src="/Thryvals/assets/log-report-steps.svg" alt="The log report's steps: every step starts as soon as its input is ready" /></p>

<p>An arrow is what a step waits for: Merge starts once all 8 Parse tasks are done, and Report once all four sections are. The dashed arrow is a notification, not a task.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">declare_report</span><span class="p">(</span><span class="n">tx</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">,</span> <span class="n">lines</span><span class="p">:</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">String</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="n">Error</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="c1">// Parse: the log cut into 8 pieces, each counted by a task of its own.</span>
    <span class="k">let</span> <span class="n">pieces</span> <span class="o">=</span> <span class="n">tx</span><span class="nf">.split_into</span><span class="p">(</span><span class="n">lines</span><span class="p">,</span> <span class="s">"Parse"</span><span class="p">,</span> <span class="n">PIECES</span><span class="p">,</span> <span class="n">PRIORITY</span><span class="p">,</span> <span class="n">parse</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>

    <span class="c1">// Merge: once all 8 are counted, adds their counts together.</span>
    <span class="k">let</span> <span class="n">merged</span> <span class="o">=</span> <span class="n">pieces</span><span class="nf">.join</span><span class="p">(</span><span class="s">"Merge"</span><span class="p">,</span> <span class="n">merge</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>

    <span class="c1">// The four sections, each written from Merge's counts, which they share.</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">group</span> <span class="o">=</span> <span class="n">merged</span><span class="nf">.fan_out</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">sections</span> <span class="o">=</span> <span class="nd">vec!</span><span class="p">[</span>
        <span class="n">group</span><span class="nf">.then</span><span class="p">(</span><span class="s">"Totals"</span><span class="p">,</span> <span class="p">|</span><span class="n">counts</span><span class="p">|</span> <span class="nf">section</span><span class="p">(</span><span class="o">&amp;</span><span class="n">counts</span><span class="p">,</span> <span class="n">totals</span><span class="p">))</span><span class="o">?</span><span class="p">,</span>
        <span class="n">group</span><span class="nf">.then</span><span class="p">(</span><span class="s">"StatusCodes"</span><span class="p">,</span> <span class="p">|</span><span class="n">counts</span><span class="p">|</span> <span class="nf">section</span><span class="p">(</span><span class="o">&amp;</span><span class="n">counts</span><span class="p">,</span> <span class="n">status_codes</span><span class="p">))</span><span class="o">?</span><span class="p">,</span>
        <span class="n">group</span><span class="nf">.then</span><span class="p">(</span><span class="s">"TopPages"</span><span class="p">,</span> <span class="p">|</span><span class="n">counts</span><span class="p">|</span> <span class="nf">section</span><span class="p">(</span><span class="o">&amp;</span><span class="n">counts</span><span class="p">,</span> <span class="n">top_pages</span><span class="p">))</span><span class="o">?</span><span class="p">,</span>
        <span class="n">group</span><span class="nf">.then</span><span class="p">(</span><span class="s">"Alerts"</span><span class="p">,</span> <span class="p">|</span><span class="n">counts</span><span class="p">|</span> <span class="nf">section</span><span class="p">(</span><span class="o">&amp;</span><span class="n">counts</span><span class="p">,</span> <span class="n">alerts</span><span class="p">))</span><span class="o">?</span><span class="p">,</span>
    <span class="p">];</span>
    <span class="n">group</span><span class="nf">.send_fanout</span><span class="p">()</span><span class="o">?</span><span class="p">;</span>

    <span class="c1">// Report: once the four are written, sends them to Console.</span>
    <span class="k">let</span> <span class="n">tx</span> <span class="o">=</span> <span class="n">tx</span><span class="nf">.clone</span><span class="p">();</span>
    <span class="n">sections</span><span class="nf">.join</span><span class="p">(</span><span class="s">"Report"</span><span class="p">,</span> <span class="k">move</span> <span class="p">|</span><span class="n">written</span><span class="p">|</span> <span class="nf">report</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tx</span><span class="p">,</span> <span class="n">written</span><span class="p">))</span><span class="o">?</span><span class="p">;</span>
    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li><strong>Parse:</strong> <code class="language-plaintext highlighter-rouge">split_into</code> cuts the lines into 8 pieces and sends a Parse task for each, as in “Splitting, joining and fanning out”.</li>
  <li><strong>Merge:</strong> <code class="language-plaintext highlighter-rouge">join</code> declares Merge, which runs once all 8 Parse tasks are done, with their counts in order.</li>
  <li><strong>The four sections:</strong> <code class="language-plaintext highlighter-rouge">fan_out</code> shares Merge’s counts with Totals, StatusCodes, TopPages and Alerts, which can run side by side.</li>
  <li><strong>Report:</strong> A second <code class="language-plaintext highlighter-rouge">join</code> declares Report, which puts the four sections together and sends the text to Console as a notification.</li>
</ul>

<p>Each step is an ordinary function, and nothing in it waits or locks: <code class="language-plaintext highlighter-rouge">parse</code> takes its share of the lines and returns <code class="language-plaintext highlighter-rouge">Result&lt;Counts, io::Error&gt;</code>. Counting can’t fail here, but an error would travel the way “Advanced tasks” describes: Merge would pass it on to the four sections, and Report would send “The report failed: …” to Console in place of the sections.</p>

<p><strong>The prime count</strong> is declared the same way, in three steps. Its plugin, Primes, splits the numbers below 10,000,000 into 8 ranges, one Sieve task each. Combine joins them, and Write, declared with <code class="language-plaintext highlighter-rouge">then</code>, sends the report to Console:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fn</span> <span class="nf">declare_count</span><span class="p">(</span><span class="n">tx</span><span class="p">:</span> <span class="o">&amp;</span><span class="n">PacketHandle</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="nb">Box</span><span class="o">&lt;</span><span class="k">dyn</span> <span class="n">Error</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="c1">// Sieve: the numbers cut into 8 ranges, each sieved by a task of its own.</span>
    <span class="k">let</span> <span class="n">sieved</span> <span class="o">=</span> <span class="n">tx</span><span class="nf">.split_into</span><span class="p">(</span><span class="nf">ranges</span><span class="p">(),</span> <span class="s">"Sieve"</span><span class="p">,</span> <span class="n">RANGES</span><span class="p">,</span> <span class="n">PRIORITY</span><span class="p">,</span> <span class="n">sieve</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>

    <span class="c1">// Combine: once all 8 are sieved, adds up what they found.</span>
    <span class="k">let</span> <span class="n">combined</span> <span class="o">=</span> <span class="n">sieved</span><span class="nf">.join</span><span class="p">(</span><span class="s">"Combine"</span><span class="p">,</span> <span class="n">combine</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>

    <span class="c1">// Write: once Combine is done, sends the report to Console.</span>
    <span class="k">let</span> <span class="n">tx</span> <span class="o">=</span> <span class="n">tx</span><span class="nf">.clone</span><span class="p">();</span>
    <span class="n">combined</span><span class="nf">.then</span><span class="p">(</span><span class="s">"Write"</span><span class="p">,</span> <span class="k">move</span> <span class="p">|</span><span class="n">found</span><span class="p">|</span> <span class="nf">write</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tx</span><span class="p">,</span> <span class="n">found</span><span class="p">))</span><span class="o">?</span><span class="p">;</span>
    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="two-projects-one-engine">Two projects, one engine</h2>

<p>Each project is a plugin, so the program’s list of projects is a list of names, each with the function that makes its plugin. Adding a project is one more entry:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="n">PROJECTS</span><span class="p">:</span> <span class="p">[(</span><span class="o">&amp;</span><span class="nb">str</span><span class="p">,</span> <span class="n">MakePlugin</span><span class="p">);</span> <span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">(</span><span class="nn">log_report</span><span class="p">::</span><span class="n">PROJECT</span><span class="p">,</span> <span class="p">|</span><span class="n">data</span><span class="p">,</span> <span class="n">tx</span><span class="p">|</span> <span class="p">{</span>
        <span class="nn">LogReader</span><span class="p">::</span><span class="nf">plugin</span><span class="p">(</span><span class="n">data</span><span class="nf">.join</span><span class="p">(</span><span class="s">"access.log"</span><span class="p">),</span> <span class="n">tx</span><span class="p">)</span>
    <span class="p">}),</span>
    <span class="p">(</span><span class="nn">primes</span><span class="p">::</span><span class="n">PROJECT</span><span class="p">,</span> <span class="p">|</span><span class="n">_data</span><span class="p">,</span> <span class="n">tx</span><span class="p">|</span> <span class="nn">Primes</span><span class="p">::</span><span class="nf">plugin</span><span class="p">(</span><span class="n">tx</span><span class="p">)),</span>
<span class="p">];</span>
</code></pre></div></div>

<p>The program adds the plugins of the projects named on its command line to one engine, or all of them when none is named. <code class="language-plaintext highlighter-rouge">cargo run -q -p thryve_examples --bin projects -- primes</code> runs the prime count alone, and neither plugin changes for that.</p>

<p>Neither project names the other. All they share is a notification: a project’s name and its report, addressed to Console. Console is an observer like Progress in “Observers”: it keeps each report until all of them are in, prints them in the order the projects were named, then sends a <code class="language-plaintext highlighter-rouge">Fin</code> task, which ends the run. So the output is the same on every run, whichever project finishes first.</p>

<h2 id="how-the-two-projects-share-the-workers">How the two projects share the workers</h2>

<p>Both projects’ tasks go into the one queue from “Inside the engine”, and the engine’s 4 worker threads take their work from it. The number of workers is one line in the configuration file the examples share: <code class="language-plaintext highlighter-rouge">worker_count = 4</code>.</p>

<ul>
  <li><strong>The report goes first:</strong> Its tasks have priority 2 and the prime count’s have priority 1, so when tasks of both are waiting, a free worker takes the report’s. Each follow-up keeps the priority of the step before it, and neither project uses the realtime levels.</li>
  <li><strong>The prime count still gets its turn:</strong> A waiting task gains one level every 3 seconds (<code class="language-plaintext highlighter-rouge">aging_interval</code>), so on a busy engine the Sieve tasks would catch up with the report’s. In this example the queue empties long before any task has waited that long.</li>
</ul>

<h2 id="what-one-engine-costs">What one engine costs</h2>

<p>Sharing an engine means sharing more than its workers:</p>

<ul>
  <li><strong>One project’s <code class="language-plaintext highlighter-rouge">FinErr</code> ends both:</strong> If LogReader can’t read the log, its entry point returns <code class="language-plaintext highlighter-rouge">FinErr</code> and the engine stops, so the prime count never prints either.</li>
  <li><strong>One place decides when the run is over:</strong> A <code class="language-plaintext highlighter-rouge">Fin</code> ends the engine for every project in it, so the projects need one decider. Here, that’s Console.</li>
  <li><strong>A panic ends the whole process,</strong> whichever project it came from. Thryve writes it to <code class="language-plaintext highlighter-rouge">Error.log</code> and exits with code 1.</li>
  <li><strong>One configuration:</strong> The projects share the number of workers, the maximum priority levels and the ageing interval.</li>
</ul>

<h2 id="not-only-rust">Not only Rust</h2>

<p>The same <code class="language-plaintext highlighter-rouge">projects</code> program is in the repo in C, C++ and Python, and all four print the same output from the same log. They call Thryve through its C API:</p>

<ul>
  <li><strong>C and C++:</strong> use the C library, with its C and C++ headers, built with CMake or GNU Make. With MSVC, CMake can also build Thryve into each program, which then runs without the DLL.</li>
  <li><strong>Python:</strong> uses <code class="language-plaintext highlighter-rouge">thryve.py</code>, the C API through <code class="language-plaintext highlighter-rouge">ctypes</code>: standard library only, nothing to install. It loads the DLL, since <code class="language-plaintext highlighter-rouge">ctypes</code> can’t load a static library.</li>
</ul>

<p>Every number in the reports is worked out with whole numbers, and every list is sorted the same way, so the four languages agree to the last character. The <a href="https://github.com/thryveframework/thryve/blob/main/crates/thryve_ffi/docs/cpp-dev-guide.md">C++ guide</a> and the <a href="https://github.com/thryveframework/thryve/blob/main/crates/thryve_ffi/docs/python-dev-guide.md">Python guide</a> show how to write programs like these.</p>

<h2 id="try-it">Try it</h2>

<p>You need Windows on x86_64. From the root of <a href="https://github.com/thryveframework/thryve">the repository</a>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo run -p thryve_examples --bin projects
cargo run -p thryve_examples --bin projects -- primes
</code></pre></div></div>

<p>The C and C++ versions build with CMake, against this repository’s build of the C library (<code class="language-plaintext highlighter-rouge">cargo build -p thryve_ffi</code>) or the download on the release page:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmake -S examples/c_cpp -B examples/c_cpp/build
cmake --build examples/c_cpp/build
</code></pre></div></div>

<p>The Python version needs the C library too, and nothing else:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo build -p thryve_ffi
python examples/python/projects.py
</code></pre></div></div>

<p>The <a href="https://github.com/thryveframework/thryve/tree/main/examples">examples’ README</a> has the rest: GNU Make, the release download, and building Thryve into the C and C++ programs.</p>

<h2 id="what-would-you-run-on-it">What would you run on it?</h2>

<p>Each project is a plugin of a few declared steps, and adding one more is one entry in a list. What would you put next to these two? Reply wherever you found this post. The code is on <a href="https://github.com/thryveframework/thryve">GitHub</a>.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Thryve is a multithreading task engine written in Rust, with a C API for C, C++ and other languages that can call C. You describe your work as tasks, and Thryve runs them on a pool of worker threads and hands each result to the step that needs it, so your program needs no thread code of its own.]]></summary></entry></feed>