Architecture¶
Internal design of the Storage COMP for contributors and curious users.
Extension Decomposition¶
The Storage COMP uses three extensions, each handling a distinct responsibility:
| Extension | File | Role |
|---|---|---|
| StorageExt | ext_storage.py |
Main engine -- connection lifecycle, transfer management, queue processing, path resolution, logging |
| BootstrapExt | ext_bootstrap.py |
Dependency installation -- locates/installs uv, creates a venv, installs firebase-admin and google-cloud-storage |
| ConnectionExt | ext_connection.py |
Circuit breaker state machine with exponential backoff |
Only StorageExt is promoted (its methods are callable directly on the COMP). The others are accessed internally via self.my.ext.BootstrapExt, etc.
Internal Operators¶
The Storage COMP network is organized into three annotation groups:
Extensions -- the three textDATs containing extension code
Data -- runtime state operators:
log-- fifoDAT (max 200 lines) for live log inspectionlog_callbacks-- textDAT (docked to log) for FIFO truncation callbacksstatus-- tableDAT with connection state, circuit state, active transfers, bucket nametransfers-- tableDAT tracking every transfer operation (upload, download, delete, list, metadata, sync)
Exec -- par_exec parameterexecuteDAT that routes button presses and parameter changes to extension methods
Threading Model¶
TouchDesigner's main thread runs at the project frame rate. The Storage COMP must perform file transfers without blocking it. Here's how threads are organized:
Main Thread (TD Cook)¶
All TouchDesigner object access happens here:
Connect()/Disconnect()-- Firebase app initialization (blocks briefly during gRPC setup)_drain_results()-- processes up to 10 items from the transfer results queue per frame- Transfer result processing, status updates, callback dispatch
- All tableDAT reads and writes
ThreadManager Pool Tasks¶
Short-lived tasks submitted to TD's built-in thread pool:
- Bootstrap --
uv venvcreation and package installation - Upload/Download -- per-file transfers, results put in
_transfer_resultsqueue - Delete/List/Metadata -- per-operation tasks, results put in
_transfer_resultsqueue - SyncFolder -- walks local dir + lists remote, compares, transfers, deletes orphans
- Reconnect sleep -- blocks for backoff delay before reconnect attempt
Pool tasks communicate results back to the main thread via queue.Queue. The main thread drains these queues every frame in _drain_results().
ThreadManager Standalone Task¶
- Keepalive -- blocks on a
threading.Eventuntil shutdown. ItsRefreshHook(called every frame on the main thread) drives_drain_results().
Main Thread ThreadManager Pool
----------- ------------------
_drain_results() <-- queue.Queue <--- _upload_worker
Connect() _download_worker
Disconnect() _delete_worker
tableDAT writes _list_worker
callback dispatch _metadata_worker
_sync_worker
_bootstrap_worker
_reconnect_sleep
Why No asyncio¶
TouchDesigner has its own event loop that is not compatible with Python's asyncio. The COMP uses threading + queue.Queue exclusively, with the queue.Queue serving as the thread-safe bridge between worker threads and the main thread.
Transfer Concurrency¶
The Max Concurrent parameter limits how many transfers run simultaneously. When the limit is reached, new transfers are queued in a deque and submitted as active ones complete.
Upload('a.jpg') --> active (slot 1 of 3)
Upload('b.jpg') --> active (slot 2 of 3)
Upload('c.jpg') --> active (slot 3 of 3)
Upload('d.jpg') --> queued (pending)
Upload('e.jpg') --> queued (pending)
...
'a.jpg' completes --> 'd.jpg' promoted to active
SyncFolder() bypasses the concurrency limit -- it runs as a single long-running pool task that handles all file operations internally.
Data Flow: Upload¶
1. Upload('photo.jpg') called on main thread
2. Path resolution: local_path + remote_path resolved against par values
3. Transfer recorded in transfers tableDAT (status: pending)
4. Concurrency check:
a. Under limit -> submit _upload_worker to ThreadManager pool (status: active)
b. At limit -> queue in _pending_transfers deque
5. _upload_worker runs in pool thread (no TD access):
- bucket.blob(remote_path).upload_from_filename(local_path)
- Optional: blob.make_public()
- Put result dict in _transfer_results queue
6. _drain_results() picks up result on main thread (next frame)
7. Update transfers tableDAT (status: complete or failed)
8. Decrement active count, submit next pending if any
9. Fire onTransferComplete callback
Data Flow: SyncFolder¶
1. SyncFolder(direction='both') called on main thread
2. Transfer recorded in transfers tableDAT (status: pending)
3. Submit _sync_worker to ThreadManager pool (bypasses concurrency limit)
4. _sync_worker runs in pool thread (no TD access):
a. Walk local directory -> build local file index (rel_path -> mtime)
b. List remote blobs -> build remote file index (rel_path -> blob)
c. Compare by name + modified time
d. Upload missing/newer local files (if direction is 'upload' or 'both')
e. Download missing/newer remote files (if direction is 'download' or 'both')
f. Delete remote orphans (if delete_remote flag set)
g. Delete local orphans (if delete_local flag set)
h. Put result dict in _transfer_results queue
5. _drain_results() picks up result on main thread
6. Update transfers tableDAT, fire onSyncComplete callback
Worker Safety¶
All module-level worker functions (_upload_worker, _download_worker, etc.) are defined at module scope and receive only plain Python objects as arguments (bucket reference, strings, queue). They never capture self, self.my, or any TouchDesigner object. This is critical -- workers run on pool threads where TD object access would cause a thread conflict crash.