Skip to content

Commit 847ee06

Browse files
authored
Merge pull request #26 from joelhelbling/feat/concurrency-analysis-docs
Analyze concurrency model and add use case documentation
2 parents 5e8ca16 + d14823a commit 847ee06

3 files changed

Lines changed: 116 additions & 0 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77

88
_"How many Ruby fibers does it take to screw in a lightbulb?"_
99

10+
## Concurrency Model
11+
12+
Shifty utilizes Ruby Fibers for cooperative multitasking. This means that all tasks (or "workers") run within a single operating system thread and explicitly yield control to one another. This model is intentionally chosen for its simplicity, which makes it easier to reason about and build sequential data processing pipelines.
13+
14+
Single-threading frees you from *preemptive* concurrency hazards (races, mutexes) within a pipeline. It does not, by itself, make the data passing between workers safe — a worker that mutates a value it was handed can still corrupt what later workers see. Governing those handoffs is a separate concern; see the planned [handoff immutability policies](docs/planning/handoff-immutability-policies.md). The Fiber model is also deliberately compatible with a future Ractor-backed worker type, rather than a rejection of parallelism.
15+
16+
For a more detailed explanation of Shifty's design and typical use cases, please see the [Use Cases Document](docs/use_cases.md).
17+
1018
## Quick Start
1119

1220
Add this line to your application's Gemfile:

docs/use_cases.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Shifty Framework: Use Cases
2+
3+
## Introduction
4+
5+
Shifty is a Ruby framework designed for building data processing pipelines. It utilizes a system of cooperatively multitasking "workers" that operate in a single thread. Each worker performs a specific task and can pass data to the next worker in a chain, allowing for the creation of complex data flows in a manageable and sequential manner.
6+
7+
## When to Use Shifty?
8+
9+
Shifty is well-suited for a variety of scenarios where data needs to be processed in a step-by-step fashion. Consider using Shifty for:
10+
11+
* **Building Data Processing Pipelines:** When your task can be broken down into a series of sequential steps, where each step transforms or enriches the data. Shifty allows you to encapsulate each step within a dedicated worker.
12+
* **ETL-like Workflows:** For scenarios that involve extracting data from a source, transforming it according to certain rules, and then loading it elsewhere. While Shifty is primarily focused on the in-application transformation aspects, it can be a valuable part of a larger ETL process within a Ruby application.
13+
* **Cooperative Multitasking Scenarios:** When you have multiple tasks that can effectively "take turns" executing. This is particularly useful if tasks involve waiting for non-blocking operations (though the current version of Shifty operates synchronously) or involve complex stateful interactions that are simpler to manage cooperatively rather than with preemptive threading.
14+
* **Simplifying Complex Sequential Logic:** If you have a long, monolithic process with many stages, Shifty can help by breaking it down into a chain of smaller, focused, and more understandable workers. This improves modularity and maintainability.
15+
* **Creating Reusable Components:** Shifty encourages the design of workers that perform specific, well-defined tasks. These workers can then be reused and recombined in different pipelines for various purposes, promoting code reuse.
16+
17+
## Example Scenarios
18+
19+
Here are a few conceptual examples of how Shifty could be applied:
20+
21+
* **Log Processing:**
22+
Imagine a pipeline for processing application logs:
23+
1. A `FileReaderWorker` reads log lines from a file.
24+
2. A `LogParserWorker` parses each line into a structured format (e.g., timestamp, level, message).
25+
3. A `ErrorFilterWorker` checks the parsed log data and only passes on entries marked as "ERROR" or "CRITICAL".
26+
4. A `ReportFormatterWorker` formats these error entries into a human-readable report.
27+
5. A `FileWriterWorker` or `EmailNotifierWorker` outputs the report.
28+
29+
* **Data Transformation Chain:**
30+
A simple pipeline demonstrating data manipulation:
31+
1. A `NumberSourceWorker` generates a sequence of numbers (e.g., 1, 2, 3, ...).
32+
2. A `MultiplierWorker` receives each number and multiplies it by 2.
33+
3. A `StringFormatterWorker` converts the multiplied number into a string, perhaps adding a prefix (e.g., "RESULT: 4").
34+
4. A `ConsoleOutputWorker` prints the final formatted string to the console.
35+
36+
* **Batch Processing:**
37+
Accumulating data into batches before further processing:
38+
1. An `ItemStreamWorker` produces individual items (e.g., from a database query or an incoming data stream).
39+
2. A `BatchWorker` (Shifty provides a `batch_worker` for this purpose) accumulates these items. It passes the accumulated batch to the next worker once a certain number of items are collected or a timeout occurs.
40+
3. A `BatchProcessorWorker` then processes the entire batch of items at once (e.g., bulk database insert, writing to a file).
41+
42+
## A Note on Values Passed Between Workers
43+
44+
The examples above pass data from one worker to the next. Because Shifty runs
45+
each value through every worker before starting the next value, workers should
46+
treat a handed-off value as read-only and express changes as new values
47+
(`arr + [x]`, `hash.merge(...)`, `value.with(...)`) rather than mutating in
48+
place (`arr <<`, `hash[k] =`, `map!`). This keeps failures local and is the
49+
direction Shifty is moving: a planned release makes deeply frozen handoffs the
50+
default, with opt-in policies for workers that genuinely need a private scratch
51+
copy or a shared mutable reference. See
52+
[handoff immutability policies](planning/handoff-immutability-policies.md) for
53+
the full design and migration guidance.
54+
55+
## Conclusion
56+
57+
Shifty aims to provide an intuitive and straightforward way to construct data processing systems within Ruby applications. By breaking down complex tasks into manageable, cooperatively multitasking workers, it helps in building modular, maintainable, and easy-to-understand data pipelines.

lib/shifty/worker.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
require "shifty/taggable"
33

44
module Shifty
5+
# Shifty::Worker uses Ruby Fibers for cooperative multitasking.
6+
# This results in a single-threaded execution model where workers
7+
# explicitly yield control to one another. This model is chosen for its
8+
# simplicity and suitability for creating chainable data processing
9+
# pipelines, where each worker performs a specific task and passes
10+
# its output to the next worker in the chain.
511
class Worker
612
attr_reader :supply, :tags
713

@@ -50,6 +56,9 @@ def ensure_ready_to_work!
5056
end
5157

5258
def workflow
59+
# This is the core of the worker's execution, managed by a Fiber.
60+
# The Fiber allows the worker to pause its execution (yield) and
61+
# be resumed later, enabling cooperative multitasking.
5362
@my_little_machine ||= Fiber.new {
5463
loop do
5564
value = supply&.shift
@@ -77,6 +86,48 @@ def task_method_exists?
7786
def task_method_accepts_a_value?
7887
method(:task).arity > 0
7988
end
89+
90+
# ## Concurrency and Thread Safety
91+
#
92+
# ### Concurrency Model
93+
# Shifty utilizes Ruby Fibers to achieve cooperative multitasking. This means
94+
# that workers voluntarily yield control, allowing other workers to execute.
95+
# The entire processing pipeline runs within a single thread, which removes
96+
# an entire class of *preemptive* concurrency hazards (races on shared
97+
# objects, the need for mutexes around Shifty's own state).
98+
#
99+
# Note that single-threading does NOT make the data flowing between workers
100+
# safe on its own: because each value passes through every worker before the
101+
# next value begins, a worker that mutates a handed-off value can silently
102+
# corrupt what downstream workers observe. That hazard is orthogonal to
103+
# threading and is addressed separately by handoff immutability policies
104+
# (see docs/planning/handoff-immutability-policies.md).
105+
#
106+
# ### Alternatives (Threads/Ractors)
107+
# Threads and Ractors are not used for parallelism *yet*. Shifty's current
108+
# goal is an easy-to-use framework for sequential data pipelines, and a
109+
# single-threaded Fiber model serves that directly without the overhead of
110+
# mutexes (Threads) or the sharing restrictions of Ractors. This is a
111+
# scoping decision, not a rejection: the planned move to deeply frozen,
112+
# shareable handoff values is deliberately Ractor-compatible and lays the
113+
# groundwork for a future Ractor-backed worker type. (Fibers cannot cross
114+
# Ractor boundaries, so such a worker would be a distinct type, not a
115+
# retrofit of this one.)
116+
#
117+
# ### Thread Safety
118+
# `Shifty::Worker` instances, and by extension `Shifty::Gang` or
119+
# `Shifty::Roster` instances that manage these workers, are **not**
120+
# inherently thread-safe if they are shared and modified across multiple
121+
# user-created native threads. If users choose to integrate Shifty components
122+
# into a multi-threaded application (e.g., processing multiple independent
123+
# Shifty pipelines in parallel using separate Threads), they are responsible
124+
# for implementing appropriate synchronization mechanisms (like mutexes)
125+
# to protect shared Shifty objects from concurrent access and modification.
126+
# (Handoff immutability policies govern only the *values* passed between
127+
# workers, not the worker/pipeline objects themselves or their closure
128+
# state — those remain the user's responsibility across threads.)
129+
# For typical use cases where a Shifty pipeline is built and run, no
130+
# external threading is usually involved by the user.
80131
end
81132

82133
class WorkerError < StandardError; end

0 commit comments

Comments
 (0)