scout-essentials 1.8.7 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.vimproject +26 -12
- data/README.md +83 -112
- data/VERSION +1 -1
- data/doc/Improvements.md +226 -0
- data/doc/StartHere.md +122 -0
- data/doc/developer/AnnotationSystem.md +184 -0
- data/doc/developer/Architecture.md +147 -0
- data/doc/developer/Configuration.md +238 -0
- data/doc/developer/CoreUtilities.md +265 -0
- data/doc/developer/DesignPrinciples.md +129 -0
- data/doc/developer/ErrorHandling.md +203 -0
- data/doc/developer/LockingAndConcurrency.md +157 -0
- data/doc/developer/PathResolution.md +200 -0
- data/doc/developer/PersistenceAndResources.md +119 -0
- data/doc/developer/StreamingModel.md +236 -0
- data/doc/user/AnnotatingData.md +202 -0
- data/doc/user/CachingResults.md +183 -0
- data/doc/user/CommandLineOptions.md +189 -0
- data/doc/user/Cookbook.md +211 -0
- data/doc/user/HandlingStreams.md +236 -0
- data/doc/user/LoggingAndProgress.md +158 -0
- data/doc/user/ProducingResources.md +177 -0
- data/doc/user/RemoteData.md +157 -0
- data/doc/user/RunningCommands.md +218 -0
- data/doc/user/WorkingWithFiles.md +217 -0
- data/lib/scout/cmd.rb +343 -40
- data/lib/scout/concurrent_stream.rb +14 -1
- data/lib/scout/indiferent_hash.rb +1 -1
- data/lib/scout/log/fingerprint.rb +13 -8
- data/lib/scout/log/progress/report.rb +1 -1
- data/lib/scout/log.rb +4 -1
- data/lib/scout/misc/digest.rb +6 -5
- data/lib/scout/misc/format.rb +24 -0
- data/lib/scout/named_array.rb +1 -1
- data/lib/scout/open/stream.rb +2 -2
- data/lib/scout/open/util.rb +8 -4
- data/lib/scout/open.rb +3 -3
- data/lib/scout/path/find.rb +3 -2
- data/lib/scout/persist.rb +14 -10
- data/lib/scout/resource/produce.rb +9 -1
- data/research/annotations-data-analysis.md +206 -0
- data/research/behavior-probes.md +1925 -0
- data/research/commands-streaming-analysis.md +272 -0
- data/research/design-philosophy-analysis.md +383 -0
- data/research/doc-audit-findings.md +294 -0
- data/research/ecosystem-attribution.md +118 -0
- data/research/implementation-inventory-core.md +1029 -0
- data/research/implementation-inventory-open.md +417 -0
- data/research/implementation-inventory-path-persist-resource.md +774 -0
- data/research/io-paths-analysis.md +228 -0
- data/research/persistence-resources-analysis.md +244 -0
- data/research/synthesis-report.md +80 -0
- data/scout-essentials.gemspec +37 -15
- data/test/scout/open/test_remote.rb +1 -2
- data/test/scout/test_cmd.rb +411 -0
- metadata +36 -14
- data/doc/Annotation.md +0 -352
- data/doc/CMD.md +0 -363
- data/doc/ConcurrentStream.md +0 -163
- data/doc/IndiferentHash.md +0 -240
- data/doc/Log.md +0 -235
- data/doc/NamedArray.md +0 -174
- data/doc/Open.md +0 -331
- data/doc/Path.md +0 -217
- data/doc/Persist.md +0 -214
- data/doc/Resource.md +0 -229
- data/doc/SimpleOPT.md +0 -236
- data/doc/TmpFile.md +0 -154
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Investigation: File I/O, Paths, and Temporary Files
|
|
2
|
+
|
|
3
|
+
**Status:** Non-normative investigation artifact. May be outdated.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
Open, Path, TmpFile, and their interactions.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## TmpFile
|
|
11
|
+
|
|
12
|
+
### What it is
|
|
13
|
+
A minimal module providing temporary file/directory paths and scoped helpers.
|
|
14
|
+
|
|
15
|
+
### Key methods
|
|
16
|
+
- `TmpFile.tmpdir` — base directory (default `~/tmp/scout/tmpfiles`).
|
|
17
|
+
- `TmpFile.user_tmp(subdir)` — user-scoped tmp directory.
|
|
18
|
+
- `TmpFile.random_name(prefix, max)` — prefix + random integer.
|
|
19
|
+
- `TmpFile.tmp_file(prefix, max, dir)` — path inside dir (defaults to tmpdir).
|
|
20
|
+
|
|
21
|
+
### Scoped helpers (create-then-cleanup)
|
|
22
|
+
- `with_file(content=nil, erase=true, options={})` — create a temporary file,
|
|
23
|
+
optionally pre-populate it with content (String, IO, or StringIO), yield the
|
|
24
|
+
path, then delete it.
|
|
25
|
+
- IO content is read in chunks via `readpartial`.
|
|
26
|
+
- Options: `:prefix`, `:max`, `:tmpdir`, `:extension`.
|
|
27
|
+
- `with_dir(erase=true, options={})` — create a temp directory, yield, cleanup.
|
|
28
|
+
- `in_dir(*args)` — create temp dir, `chdir` into it, yield, cleanup.
|
|
29
|
+
|
|
30
|
+
### Persistence path helper
|
|
31
|
+
- `tmp_for_file(file, tmp_options={}, other_options={})` — generates a stable
|
|
32
|
+
cache filename from a logical filename + options.
|
|
33
|
+
- Replaces `/` with `·` (SLASH_REPLACE) to make a single filename.
|
|
34
|
+
- Truncates names over 150 chars, appends a digest.
|
|
35
|
+
- Appends a digest of `other_options` for parameter-variant uniqueness.
|
|
36
|
+
- Used pervasively by Persist to generate cache paths.
|
|
37
|
+
|
|
38
|
+
### Design insight
|
|
39
|
+
TmpFile is the **fundamental cache-path primitive** — it turns logical names
|
|
40
|
+
into stable filesystem paths. It does not itself manage caching or locking;
|
|
41
|
+
it just builds deterministic paths.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Open
|
|
46
|
+
|
|
47
|
+
### What it is
|
|
48
|
+
A unified, high-level file/stream/remote I/O module. It wraps plain File I/O,
|
|
49
|
+
streaming helpers, remote fetching (wget/ssh), atomic writes, locking,
|
|
50
|
+
compression auto-handling, and filesystem utilities (grep, sort, collapse,
|
|
51
|
+
wc, head, tail, mv, cp, rm).
|
|
52
|
+
|
|
53
|
+
### Sub-modules
|
|
54
|
+
- `Open/stream.rb` — streaming, tee streams, `sensible_write`, monitor streams.
|
|
55
|
+
- `Open/util.rb` — existence checks, remote detection, compression detection,
|
|
56
|
+
realpath, gzip/bgzip/zip helpers, grep, sort, wc, head, tail, mv, cp, rm,
|
|
57
|
+
is_gzip?/is_bgzip?/is_zip? detection.
|
|
58
|
+
- `Open/remote.rb` — remote detection, `wget` download, `ssh` file operations
|
|
59
|
+
(exists, open, read, write, mv, cp, upload, download), scp.
|
|
60
|
+
- `Open/lock/lockfile.rb` — cross-process locking via lock files (a port of
|
|
61
|
+
the Lockfile gem). Supports `Open.lock(file) { ... }`.
|
|
62
|
+
- `Open/final.rb` — streaming wrappers with guaranteed resource cleanup
|
|
63
|
+
(`stream_without_close`, etc.).
|
|
64
|
+
|
|
65
|
+
### Key patterns
|
|
66
|
+
|
|
67
|
+
#### Auto-decompression
|
|
68
|
+
`Open.read("data.tsv.gz")` automatically detects `.gz`, `.bgz`, `.zip` and
|
|
69
|
+
decompresses transparently. `Open.open` also auto-detects compression on the
|
|
70
|
+
fly via magic byte detection.
|
|
71
|
+
|
|
72
|
+
#### Remote access
|
|
73
|
+
- `Open.remote?(path)` — detects `http://`, `https://`, `ssh:` prefixes.
|
|
74
|
+
- `Open.open(remote_url)` — downloads via `wget` to a cache, then opens.
|
|
75
|
+
- SSH-style: `Open.exists?("server:file")`, `Open.read("server:file")` etc.
|
|
76
|
+
|
|
77
|
+
#### Atomic writes (sensible_write)
|
|
78
|
+
`Open.sensible_write(file, content)` writes to a temporary file then renames
|
|
79
|
+
atomically. Uses a lock to prevent concurrent writes to the same target.
|
|
80
|
+
This is the safe way to write files that might be read concurrently.
|
|
81
|
+
|
|
82
|
+
#### Cross-process locking
|
|
83
|
+
`Open.lock(lockfile) { ... }` — file-based locks using `flock` + lock files.
|
|
84
|
+
Used by Persist and Resource for concurrent-safe resource production.
|
|
85
|
+
|
|
86
|
+
#### Stream handling
|
|
87
|
+
- `Open.open(path) { |io| ... }` — streaming file open with auto-decompression.
|
|
88
|
+
- `Open.stream(io)` — wraps an IO into a ConcurrentStream for lifecycle safety.
|
|
89
|
+
- `Open.tee_stream(io)` — splits a stream into multiple consumers.
|
|
90
|
+
- `Open.consume_stream(io, close=true)` — reads to end and closes.
|
|
91
|
+
|
|
92
|
+
#### Grep/sort/wc utilities
|
|
93
|
+
`Open.grep(stream, pattern)`, `Open.sort(stream)`, `Open.wc(stream)`,
|
|
94
|
+
`Open.head(stream, n)`, `Open.collapse_stream(stream)` — stream-processing
|
|
95
|
+
utilities that work on file paths or IO objects.
|
|
96
|
+
|
|
97
|
+
### Compression detection
|
|
98
|
+
- `Open.is_gzip?(file)` — magic bytes `\x1f\x8b`.
|
|
99
|
+
- `Open.is_bgzip?(file)` — magic bytes `\x1f\x8b` + extra field in header.
|
|
100
|
+
- `Open.is_zip?(file)` — magic bytes `PK\x03\x04`.
|
|
101
|
+
- `Open.gzip?(file)` — file extension `.gz`.
|
|
102
|
+
- `Open.bgzip?(file)` — file extension `.bgz`.
|
|
103
|
+
- `Open.zip?(file)` — file extension `.zip`.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Path
|
|
108
|
+
|
|
109
|
+
### What it is
|
|
110
|
+
A path abstraction layered on top of Annotation. Paths are annotated Strings
|
|
111
|
+
that know how to **find** themselves across a configurable set of locations
|
|
112
|
+
(path maps).
|
|
113
|
+
|
|
114
|
+
### Annotations
|
|
115
|
+
- `pkgdir` — logical package name (default 'scout').
|
|
116
|
+
- `libdir` — library directory for resolving `lib` maps.
|
|
117
|
+
- `path_maps` — hash of named location templates.
|
|
118
|
+
- `map_order` — ordered list of map names to try.
|
|
119
|
+
- `where` — which map resolved successfully during `find`.
|
|
120
|
+
- `original` — the path before resolution.
|
|
121
|
+
|
|
122
|
+
### Setup
|
|
123
|
+
- `Path.setup(string, pkgdir, libdir, path_maps, map_order)` — extends a plain
|
|
124
|
+
String with Path annotations.
|
|
125
|
+
- A Path **is** a String; it responds to all String methods plus path-specific
|
|
126
|
+
ones.
|
|
127
|
+
|
|
128
|
+
### Path maps system
|
|
129
|
+
Path maps are templates with placeholders:
|
|
130
|
+
- `{PKGDIR}`, `{HOME}`, `{PWD}`, `{TOPLEVEL}`, `{SUBPATH}`, `{PATH}`,
|
|
131
|
+
`{BASENAME}`, `{LIBDIR}`, `{RESOURCE}`, `{MAPNAME}`.
|
|
132
|
+
|
|
133
|
+
Default maps:
|
|
134
|
+
```ruby
|
|
135
|
+
:current => "{PWD}/{TOPLEVEL}/{SUBPATH}"
|
|
136
|
+
:home => "{HOME}/{TOPLEVEL}/{PKGDIR}/{SUBPATH}"
|
|
137
|
+
:user => "{HOME}/.{PKGDIR}/{TOPLEVEL}/{SUBPATH}"
|
|
138
|
+
:global => '/{TOPLEVEL}/{PKGDIR}/{SUBPATH}'
|
|
139
|
+
:lib => '{LIBDIR}/{TOPLEVEL}/{SUBPATH}'
|
|
140
|
+
# etc.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Resolution: `find`
|
|
144
|
+
```ruby
|
|
145
|
+
path = Path.setup("data/file.tsv", "mypkg")
|
|
146
|
+
path.find # tries each map in map_order, returns first existing location
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- If the path is already located (absolute, ~/, ./), `find` returns immediately.
|
|
150
|
+
- Otherwise, it iterates `map_order`, applies each map template, and checks
|
|
151
|
+
existence (including `.gz`/`.bgz` alternatives).
|
|
152
|
+
- The first match wins; `where` annotation records which map resolved.
|
|
153
|
+
|
|
154
|
+
### Method-missing as path builder
|
|
155
|
+
```ruby
|
|
156
|
+
path = Path.setup("data", "mypkg")
|
|
157
|
+
path.file # → "data/file" (as annotated Path)
|
|
158
|
+
path.data.tsv # → "data/data/tsv"
|
|
159
|
+
```
|
|
160
|
+
This works because `method_missing(name, prev)` calls `join(name, prev)`,
|
|
161
|
+
which returns an annotated Path. This is the idiomatic Scout way to build
|
|
162
|
+
nested paths.
|
|
163
|
+
|
|
164
|
+
### Operators
|
|
165
|
+
- `path / "subpath"` — alias for `join`.
|
|
166
|
+
- `path["subpath"]` — alias for `join`.
|
|
167
|
+
- `path.join("subpath")` — returns a new annotated Path.
|
|
168
|
+
|
|
169
|
+
### located? check
|
|
170
|
+
`Path.located?(path)` — returns true if path starts with `/`, `~/`, or `./`.
|
|
171
|
+
These paths are treated as already resolved and are not subject to map
|
|
172
|
+
resolution.
|
|
173
|
+
|
|
174
|
+
### Path manipulation
|
|
175
|
+
- `_parts` — split by `/`.
|
|
176
|
+
- `_toplevel` — first path component.
|
|
177
|
+
- `_subpath` — everything after the first `/`.
|
|
178
|
+
- `set_extension(ext)` — swap file extension.
|
|
179
|
+
- `set_extension` — used by `find_with_extension`.
|
|
180
|
+
|
|
181
|
+
### find_with_extension
|
|
182
|
+
```ruby
|
|
183
|
+
path.find_with_extension(['tsv', 'csv'], produce: true)
|
|
184
|
+
```
|
|
185
|
+
Tries the path as-is, then tries each extension. Useful when the exact
|
|
186
|
+
extension is not known.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Cross-module interactions
|
|
191
|
+
|
|
192
|
+
- **Path depends on Annotation** — extends Annotation; paths are annotated strings.
|
|
193
|
+
- **Path depends on IndiferentHash** — path_maps is an IndiferentHash.
|
|
194
|
+
- **Open depends on Path** — for path resolution in read/write operations.
|
|
195
|
+
- **Open depends on ConcurrentStream** — for streaming safety.
|
|
196
|
+
- **TmpFile depends on Open** — uses `Open.mkdir` to ensure directories exist.
|
|
197
|
+
- **Resource depends on Path and Open** — resources are annotated paths that
|
|
198
|
+
can be produced on demand.
|
|
199
|
+
- **Persist depends on TmpFile** — uses `tmp_for_file` to generate cache paths.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Gotchas and warnings
|
|
204
|
+
|
|
205
|
+
1. **Path.map_order is a class variable (@@map_order)** — modifying it globally
|
|
206
|
+
affects all Path instances. Use instance-level `@map_order` for per-resource
|
|
207
|
+
customization.
|
|
208
|
+
2. **Path.method_missing sends `to_*` methods to super** — `path.to_s` works
|
|
209
|
+
normally, but any custom method starting with `to_` is passed through.
|
|
210
|
+
3. **Open.remote? requires the full URL prefix** — bare hostnames without
|
|
211
|
+
`http://` or `ssh:` are treated as local paths.
|
|
212
|
+
3. **Open.sensible_write lock files are not always cleaned up** — if the
|
|
213
|
+
process crashes during write, lock files may persist in
|
|
214
|
+
`tmp/sensible_write_locks`.
|
|
215
|
+
4. **Path.find may return a path that doesn't exist** — if no map resolves to
|
|
216
|
+
an existing file, `find` returns the `:default` map result (which may not
|
|
217
|
+
exist). Always check `exist?` or `File.exist?` after `find`.
|
|
218
|
+
5. **TmpFile.tmp_for_file generates Path-annotated strings** — the return value
|
|
219
|
+
is a Path, not a plain String, so it responds to `find`, `produce`, etc.
|
|
220
|
+
6. **Open.open auto-decompresses based on file content, not just extension** —
|
|
221
|
+
if a file named `.tsv` actually contains gzipped data, Open.open will
|
|
222
|
+
still decompress it.
|
|
223
|
+
7. **ConcurrentStream.setup on streams returned by Open** — when you use
|
|
224
|
+
`Open.open` with a block, the stream is automatically set up with
|
|
225
|
+
ConcurrentStream. When using `Open.read`, the stream is consumed and
|
|
226
|
+
closed.
|
|
227
|
+
8. **Open.read on a directory** — returns an empty string or raises, depending
|
|
228
|
+
on the Ruby version.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Investigation: Persistence, Resources, and Concurrency
|
|
2
|
+
|
|
3
|
+
**Status:** Non-normative investigation artifact. May be outdated.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
Persist, Resource, ConcurrentStream, and their interactions.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Persist
|
|
11
|
+
|
|
12
|
+
### What it is
|
|
13
|
+
A caching module that saves the result of expensive computations to disk (or
|
|
14
|
+
memory), keyed by a deterministic cache path. It provides type-aware
|
|
15
|
+
serialization and deserialization, multiple serialization drivers, and
|
|
16
|
+
concurrent-safe production via file locking.
|
|
17
|
+
|
|
18
|
+
### Core pattern: `persist` / `persist`-with-block
|
|
19
|
+
```ruby
|
|
20
|
+
result = Persist.persist("my_data", :string, :path => "/cache/my_data") do
|
|
21
|
+
expensive_computation()
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
- If `/cache/my_data` exists, the stored value is loaded.
|
|
25
|
+
- If not, the block is executed, and the result is saved.
|
|
26
|
+
- The `:path` determines where on disk.
|
|
27
|
+
- The type (`:string`, `:marshal`, `:array`, etc.) determines serialization.
|
|
28
|
+
|
|
29
|
+
### Serialization types
|
|
30
|
+
Supported by `serialize`/`deserialize`:
|
|
31
|
+
- `:string`, `:text`, `:integer`, `:float`, `:boolean` — basic type coercion.
|
|
32
|
+
- `:array` — newline-joined.
|
|
33
|
+
- `:yaml`, `:json`, `:marshal` — standard serializers.
|
|
34
|
+
- `:binary` — raw bytes.
|
|
35
|
+
- `:file` — stores a path to another file (pointer).
|
|
36
|
+
- `:annotation`, `:annotations` — via `Annotation.tsv`.
|
|
37
|
+
- `<type>_array` — array of the inner type (e.g., `:integer_array`).
|
|
38
|
+
- `:memory` — uses the in-memory `Persist::MEMORY` hash.
|
|
39
|
+
- Custom drivers — registered via `save_drivers`/`load_drivers` hashes.
|
|
40
|
+
|
|
41
|
+
### `persist` lifecycle
|
|
42
|
+
1. Generate a cache path (via `persist_path` if not given).
|
|
43
|
+
2. Check if the cache file exists.
|
|
44
|
+
3. If it exists: load and return.
|
|
45
|
+
4. If not: acquire a lock (`Open.lock`), check again (double-check locking),
|
|
46
|
+
execute the block, save the result, release the lock, return.
|
|
47
|
+
|
|
48
|
+
### `persist_path` generation
|
|
49
|
+
When `:path` is not explicitly provided, Persist builds one from the name and
|
|
50
|
+
options using `TmpFile.tmp_for_file`:
|
|
51
|
+
```ruby
|
|
52
|
+
Persist.persist_path("my_data", :some_option => "value")
|
|
53
|
+
# => ~/tmp/scout/tmpfiles/my_data·SOME_OPTION=VALUE
|
|
54
|
+
```
|
|
55
|
+
This means the cache path is deterministic for the same name + options,
|
|
56
|
+
enabling automatic cache reuse.
|
|
57
|
+
|
|
58
|
+
### Concurrency model
|
|
59
|
+
- `Open.lock(cache_path + '.lock')` — uses lockfiles for cross-process safety.
|
|
60
|
+
- Double-check: checks existence again inside the lock.
|
|
61
|
+
- Multiple processes attempting to produce the same cache will block on the
|
|
62
|
+
lock, and the second will find the file already present.
|
|
63
|
+
|
|
64
|
+
### `persist_tsv` (legacy)
|
|
65
|
+
`Persist.persist_tsv(database, file, data, type)` is a convenience for caching
|
|
66
|
+
TSV-style data. It wraps the persist pattern with TSV-specific serialization.
|
|
67
|
+
In scout-essentials, this exists but the main TSV class is not included —
|
|
68
|
+
this method is for integration with downstream TSV-using code.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Resource
|
|
73
|
+
|
|
74
|
+
### What it is
|
|
75
|
+
A module for declaring **named resources** that can be produced on demand.
|
|
76
|
+
A Resource module acts as a namespace: it defines a root path (where files
|
|
77
|
+
live), claims (how to produce each file), and a path resolution scheme.
|
|
78
|
+
|
|
79
|
+
### Anatomy of a Resource module
|
|
80
|
+
```ruby
|
|
81
|
+
module MyResource
|
|
82
|
+
extend Resource
|
|
83
|
+
self.claim Path.setup("data/file.tsv"), :proc do |path|
|
|
84
|
+
# produce the file at `path`
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
- `extend Resource` — becomes a resource namespace.
|
|
89
|
+
- `claim(path, type, content = nil, &block)` — declare how to produce a path.
|
|
90
|
+
- `self.root` — the filesystem root for this resource.
|
|
91
|
+
- `self.pkgdir` — package name for path resolution.
|
|
92
|
+
- `self.subdir` — subdirectory under the root.
|
|
93
|
+
|
|
94
|
+
### Claim types
|
|
95
|
+
1. `:string` — write the string content to the path.
|
|
96
|
+
2. `:url` — download the URL to the path.
|
|
97
|
+
3. `:proc` — call the block; it returns String/IO/Array/TSV to write.
|
|
98
|
+
- `arity == 0` → block takes no args, returns data.
|
|
99
|
+
- `arity == 1` → block receives the output path.
|
|
100
|
+
4. `:rake` — run a Rake task (with `ScoutRake.run`).
|
|
101
|
+
5. `:install` — install software (uses `Resource.install`).
|
|
102
|
+
6. `:csv` — (declared but not implemented — raises).
|
|
103
|
+
|
|
104
|
+
### Production lifecycle (`produce`)
|
|
105
|
+
1. Resolve the path to find its current location (`path.find`).
|
|
106
|
+
2. If the file exists at the resolved location, return.
|
|
107
|
+
3. If a claim exists for this path:
|
|
108
|
+
- Acquire a lock (`Open.lock`).
|
|
109
|
+
- Re-check existence (double-check).
|
|
110
|
+
- Execute the claim (write string, download URL, run proc, run rake, etc.).
|
|
111
|
+
- If the claim type is missing, try `.gz` / `.bgz` variants.
|
|
112
|
+
4. On error: remove the partial file and re-raise.
|
|
113
|
+
5. After production, reset the path's location cache.
|
|
114
|
+
|
|
115
|
+
### Resource → Path integration
|
|
116
|
+
The `Resource/path.rb` file adds `produce`, `produce_and_find`, `produce_with_extension`,
|
|
117
|
+
`relocate`, `identify`, `open`, `read`, `write`, `list`, `exists?`, and
|
|
118
|
+
`find_with_extension` methods to all Path objects.
|
|
119
|
+
|
|
120
|
+
This means **any Path can be produced on-demand** if its `pkgdir` is a
|
|
121
|
+
Resource module. This is the key integration: `path.produce` checks existence,
|
|
122
|
+
and if the file is missing, triggers production.
|
|
123
|
+
|
|
124
|
+
### Software installation (`Resource/software.rb`)
|
|
125
|
+
- `Resource.install(url_or_spec, name, software_dir)` — downloads and extracts
|
|
126
|
+
software packages.
|
|
127
|
+
- `set_software_env(software_dir)` — adds installed software to PATH and
|
|
128
|
+
LD_LIBRARY_PATH.
|
|
129
|
+
- Used by the `:install` claim type.
|
|
130
|
+
|
|
131
|
+
### Resource relocation and identification
|
|
132
|
+
- `Resource.relocate(path)` — moves a resource to a better location based on
|
|
133
|
+
resource identification.
|
|
134
|
+
- `Resource.identify(path)` — determines the canonical identity of a path.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## ConcurrentStream
|
|
139
|
+
|
|
140
|
+
### What it is
|
|
141
|
+
A mixin that augments IO-like objects (pipes from `CMD`, file streams from
|
|
142
|
+
`Open`, in-memory streams) with concurrency-aware lifecycle management:
|
|
143
|
+
thread/pid tracking, join/abort semantics, callback chains, and error
|
|
144
|
+
propagation.
|
|
145
|
+
|
|
146
|
+
### Core attributes
|
|
147
|
+
- `threads` — threads that produce the stream's data.
|
|
148
|
+
- `pids` — subprocess PIDs that produce the stream's data.
|
|
149
|
+
- `callback` — Proc chain called on successful `join`.
|
|
150
|
+
- `abort_callback` — Proc chain called on `abort`.
|
|
151
|
+
- `autojoin` — if true, the stream auto-joins on close/eof.
|
|
152
|
+
- `no_fail` — if true, suppress process failures (log instead of raise).
|
|
153
|
+
- `pair` — a paired stream (e.g., stdout/stderr pair) that is aborted together.
|
|
154
|
+
- `lock` — an associated lock released on join/abort.
|
|
155
|
+
- `stream_exception` — exception captured during streaming; raised on join.
|
|
156
|
+
- `joined`, `aborted` — state flags.
|
|
157
|
+
|
|
158
|
+
### Lifecycle: join
|
|
159
|
+
1. `join_threads` — wait for all producer threads; check `Process::Status` for
|
|
160
|
+
failures; raise `ConcurrentStreamProcessFailed` on failure (unless `no_fail`).
|
|
161
|
+
2. `join_pids` — `Process.waitpid` for each PID; check exit status.
|
|
162
|
+
3. Check `stream_exception` — raise if set.
|
|
163
|
+
4. `join_callback` — call the callback chain.
|
|
164
|
+
5. `close` — close the underlying IO.
|
|
165
|
+
6. Release the lock.
|
|
166
|
+
|
|
167
|
+
### Lifecycle: abort
|
|
168
|
+
1. Set `stream_exception` if not already set.
|
|
169
|
+
2. Call `abort_callback` chain.
|
|
170
|
+
3. `abort_threads` — raise `Aborted` exception in each producer thread.
|
|
171
|
+
4. `abort_pids` — send `SIGINT` to each PID.
|
|
172
|
+
5. Abort the pair stream if present.
|
|
173
|
+
6. Close the IO.
|
|
174
|
+
7. Release the lock.
|
|
175
|
+
|
|
176
|
+
### Callback chaining
|
|
177
|
+
`ConcurrentStream.setup(stream, :callback => proc1)` then
|
|
178
|
+
`ConcurrentStream.setup(stream, :callback => proc2)` results in a chained
|
|
179
|
+
callback that calls `proc1` then `proc2`. This allows multiple subsystems to
|
|
180
|
+
attach cleanup logic to the same stream without coordination.
|
|
181
|
+
|
|
182
|
+
### Annotate (propagation)
|
|
183
|
+
`ConcurrentStream#annotate(stream)` copies threads, pids, callback, etc. to
|
|
184
|
+
another stream. Used by CMD when transforming streams (e.g., decompressing).
|
|
185
|
+
|
|
186
|
+
### `process_stream` class method
|
|
187
|
+
`ConcurrentStream.process_stream(stream, **kwargs) { ... }` — a convenience
|
|
188
|
+
wrapper that sets up the stream, runs the block, and ensures proper
|
|
189
|
+
join/abort in case of exception. Used by stream-consuming code for
|
|
190
|
+
guaranteed cleanup.
|
|
191
|
+
|
|
192
|
+
### AbortedStream
|
|
193
|
+
`AbortedStream.setup(obj, exception)` — marks a stream as aborted, attaching
|
|
194
|
+
the exception. Consumers can check `AbortedStream === stream` and access
|
|
195
|
+
`stream.exception`.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Cross-module interactions
|
|
200
|
+
|
|
201
|
+
- **Persist depends on Open** — for file I/O, locking, and atomic writes.
|
|
202
|
+
- **Persist depends on TmpFile** — for cache path generation.
|
|
203
|
+
- **Resource depends on Open, Path, TmpFile** — for production, path
|
|
204
|
+
resolution, and locking.
|
|
205
|
+
- **CMD depends on ConcurrentStream** — CMD.cmd returns ConcurrentStream-enhanced
|
|
206
|
+
IO objects that track the subprocess PID and threads.
|
|
207
|
+
- **Open depends on ConcurrentStream** — Open.stream wraps IO objects with
|
|
208
|
+
ConcurrentStream for lifecycle safety.
|
|
209
|
+
- **ConcurrentStream depends on IndiferentHash** — for `process_options`.
|
|
210
|
+
- **Log::ProgressBar depends on CMD** — `guess_obj_max` uses `CMD.cmd("wc -l")`
|
|
211
|
+
to estimate total lines in a file.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Gotchas and warnings
|
|
216
|
+
|
|
217
|
+
1. **Persist.persist silently returns nil on failure** — if the block returns
|
|
218
|
+
`nil`, `Persist.save` returns immediately without writing. The cache file
|
|
219
|
+
is not created, so the next call will re-execute the block.
|
|
220
|
+
2. **Persist MEMORY type is process-local** — `:memory` persistence uses a
|
|
221
|
+
global `Persist::MEMORY` hash, which is lost when the process exits. It is
|
|
222
|
+
suitable only for within-process caching.
|
|
223
|
+
3. **Resource.produce fallback to .gz/.bgz** — if no claim matches, produce
|
|
224
|
+
tries `path + '.gz'` then `path + '.bgz'`. This is usually desired but can
|
|
225
|
+
cause confusing errors if you expected a specific extension.
|
|
226
|
+
4. **ConcurrentStream.join is idempotent** — calling `join` on an already-joined
|
|
227
|
+
stream is safe (returns without re-joining), but the `@joined` flag is set
|
|
228
|
+
even if the join raised an exception.
|
|
229
|
+
5. **ConcurrentStream callback chains can grow** — each `setup` call with a
|
|
230
|
+
`:callback` prepends to the chain. Long chains from repeated `setup` calls
|
|
231
|
+
may cause unexpected ordering.
|
|
232
|
+
6. **CMD no_fail streams** — a stream with `no_fail: true` will log errors but
|
|
233
|
+
not raise. Reading from it may produce truncated output. Check
|
|
234
|
+
`stream.exit_status` if you need to verify success.
|
|
235
|
+
7. **Open.lock requires the lock directory to exist** — if the directory does
|
|
236
|
+
not exist, the lock file cannot be created and the lock fails silently.
|
|
237
|
+
8. **Resource :csv claim type raises NotImplementedError** — declared but
|
|
238
|
+
not implemented. If you encounter this, use `:proc` with manual CSV parsing.
|
|
239
|
+
9. **Persist caching of nil results** — if the computation block returns `nil`,
|
|
240
|
+
no cache file is written, leading to re-computation on every call. Use a
|
|
241
|
+
sentinel value if nil is a valid result.
|
|
242
|
+
10. **ConcurrentStream.pair abort propagation** — aborting a stream also aborts
|
|
243
|
+
its pair. If you only want to abort one side of a stdout/stderr pair, you
|
|
244
|
+
must detach the pair first (set `pair = nil`).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Synthesis Report
|
|
2
|
+
|
|
3
|
+
**Status:** Non-normative investigation artifact. May be outdated.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Cross-check all investigation artifacts for consistency, overlaps, and gaps.
|
|
8
|
+
Produce a concrete file mapping from investigation content to target
|
|
9
|
+
documentation files.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Module → Concept → Documentation mapping
|
|
14
|
+
|
|
15
|
+
| Module | Concept | User doc | Developer doc |
|
|
16
|
+
|--------|---------|----------|---------------|
|
|
17
|
+
| Annotation | Annotating objects | AnnotatingData.md | AnnotationSystem.md |
|
|
18
|
+
| NamedArray | Named arrays | AnnotatingData.md | AnnotationSystem.md |
|
|
19
|
+
| IndiferentHash | Key-indifferent hashes | AnnotatingData.md | AnnotationSystem.md |
|
|
20
|
+
| Open | File I/O | WorkingWithFiles.md | PathResolution.md |
|
|
21
|
+
| Path | Path resolution | WorkingWithFiles.md | PathResolution.md |
|
|
22
|
+
| TmpFile | Temp files | WorkingWithFiles.md | PathResolution.md |
|
|
23
|
+
| CMD | Running commands | RunningCommands.md | (covered in user doc) |
|
|
24
|
+
| Log | Logging | LoggingAndProgress.md | (covered in user doc) |
|
|
25
|
+
| ConcurrentStream | Stream lifecycle | HandlingStreams.md | StreamingModel.md |
|
|
26
|
+
| Persist | Caching | CachingResults.md | PersistenceAndResources.md |
|
|
27
|
+
| Resource | On-demand resources | ProducingResources.md | PersistenceAndResources.md |
|
|
28
|
+
| SOPT | CLI options | CommandLineOptions.md | (covered in user doc) |
|
|
29
|
+
| Misc | Utility functions | (cross-cutting) | DesignPrinciples.md |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Target documentation structure
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
doc/
|
|
37
|
+
StartHere.md
|
|
38
|
+
Improvements.md
|
|
39
|
+
user/
|
|
40
|
+
AnnotatingData.md
|
|
41
|
+
WorkingWithFiles.md
|
|
42
|
+
RunningCommands.md
|
|
43
|
+
LoggingAndProgress.md
|
|
44
|
+
HandlingStreams.md
|
|
45
|
+
CachingResults.md
|
|
46
|
+
ProducingResources.md
|
|
47
|
+
CommandLineOptions.md
|
|
48
|
+
Cookbook.md
|
|
49
|
+
developer/
|
|
50
|
+
Architecture.md
|
|
51
|
+
DesignPrinciples.md
|
|
52
|
+
AnnotationSystem.md
|
|
53
|
+
PathResolution.md
|
|
54
|
+
PersistenceAndResources.md
|
|
55
|
+
StreamingModel.md
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Writing order (by priority)
|
|
61
|
+
|
|
62
|
+
1. **StartHere.md** — entry point
|
|
63
|
+
2. **developer/Architecture.md** — mental map
|
|
64
|
+
3. **developer/DesignPrinciples.md** — coding style
|
|
65
|
+
4. **user/AnnotatingData.md** — Annotation foundation
|
|
66
|
+
5. **user/WorkingWithFiles.md** — most used
|
|
67
|
+
6. **user/RunningCommands.md** — CMD
|
|
68
|
+
7. **user/LoggingAndProgress.md** — Log
|
|
69
|
+
8. **user/HandlingStreams.md** — ConcurrentStream
|
|
70
|
+
9. **user/CachingResults.md** — Persist
|
|
71
|
+
10. **user/ProducingResources.md** — Resource
|
|
72
|
+
11. **user/CommandLineOptions.md** — SOPT
|
|
73
|
+
12. **user/Cookbook.md** — recipes
|
|
74
|
+
13. **developer/AnnotationSystem.md** — internals
|
|
75
|
+
14. **developer/PathResolution.md** — path maps
|
|
76
|
+
15. **developer/PersistenceAndResources.md** — caching internals
|
|
77
|
+
16. **developer/StreamingModel.md** — stream internals
|
|
78
|
+
17. **Improvements.md** — recommendations
|
|
79
|
+
18. **research/** — curate SHARED artifacts
|
|
80
|
+
19. **Validation** — check links, coverage
|
data/scout-essentials.gemspec
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
|
5
|
-
# stub: scout-essentials 1.
|
|
5
|
+
# stub: scout-essentials 1.9.0 ruby lib
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |s|
|
|
8
8
|
s.name = "scout-essentials".freeze
|
|
9
|
-
s.version = "1.
|
|
9
|
+
s.version = "1.9.0".freeze
|
|
10
10
|
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
12
12
|
s.require_paths = ["lib".freeze]
|
|
@@ -26,18 +26,28 @@ Gem::Specification.new do |s|
|
|
|
26
26
|
"README.md",
|
|
27
27
|
"Rakefile",
|
|
28
28
|
"VERSION",
|
|
29
|
-
"doc/
|
|
30
|
-
"doc/
|
|
31
|
-
"doc/
|
|
32
|
-
"doc/
|
|
33
|
-
"doc/
|
|
34
|
-
"doc/
|
|
35
|
-
"doc/
|
|
36
|
-
"doc/
|
|
37
|
-
"doc/
|
|
38
|
-
"doc/
|
|
39
|
-
"doc/
|
|
40
|
-
"doc/
|
|
29
|
+
"doc/Improvements.md",
|
|
30
|
+
"doc/StartHere.md",
|
|
31
|
+
"doc/developer/AnnotationSystem.md",
|
|
32
|
+
"doc/developer/Architecture.md",
|
|
33
|
+
"doc/developer/Configuration.md",
|
|
34
|
+
"doc/developer/CoreUtilities.md",
|
|
35
|
+
"doc/developer/DesignPrinciples.md",
|
|
36
|
+
"doc/developer/ErrorHandling.md",
|
|
37
|
+
"doc/developer/LockingAndConcurrency.md",
|
|
38
|
+
"doc/developer/PathResolution.md",
|
|
39
|
+
"doc/developer/PersistenceAndResources.md",
|
|
40
|
+
"doc/developer/StreamingModel.md",
|
|
41
|
+
"doc/user/AnnotatingData.md",
|
|
42
|
+
"doc/user/CachingResults.md",
|
|
43
|
+
"doc/user/CommandLineOptions.md",
|
|
44
|
+
"doc/user/Cookbook.md",
|
|
45
|
+
"doc/user/HandlingStreams.md",
|
|
46
|
+
"doc/user/LoggingAndProgress.md",
|
|
47
|
+
"doc/user/ProducingResources.md",
|
|
48
|
+
"doc/user/RemoteData.md",
|
|
49
|
+
"doc/user/RunningCommands.md",
|
|
50
|
+
"doc/user/WorkingWithFiles.md",
|
|
41
51
|
"lib/scout-essentials.rb",
|
|
42
52
|
"lib/scout/annotation.rb",
|
|
43
53
|
"lib/scout/annotation/annotated_object.rb",
|
|
@@ -104,6 +114,18 @@ Gem::Specification.new do |s|
|
|
|
104
114
|
"lib/scout/simple_opt/parse.rb",
|
|
105
115
|
"lib/scout/simple_opt/setup.rb",
|
|
106
116
|
"lib/scout/tmpfile.rb",
|
|
117
|
+
"research/annotations-data-analysis.md",
|
|
118
|
+
"research/behavior-probes.md",
|
|
119
|
+
"research/commands-streaming-analysis.md",
|
|
120
|
+
"research/design-philosophy-analysis.md",
|
|
121
|
+
"research/doc-audit-findings.md",
|
|
122
|
+
"research/ecosystem-attribution.md",
|
|
123
|
+
"research/implementation-inventory-core.md",
|
|
124
|
+
"research/implementation-inventory-open.md",
|
|
125
|
+
"research/implementation-inventory-path-persist-resource.md",
|
|
126
|
+
"research/io-paths-analysis.md",
|
|
127
|
+
"research/persistence-resources-analysis.md",
|
|
128
|
+
"research/synthesis-report.md",
|
|
107
129
|
"scout-essentials.gemspec",
|
|
108
130
|
"share/color/color_names",
|
|
109
131
|
"share/color/diverging_colors.hex",
|
|
@@ -159,7 +181,7 @@ Gem::Specification.new do |s|
|
|
|
159
181
|
]
|
|
160
182
|
s.homepage = "http://github.com/mikisvaz/scout-essentials".freeze
|
|
161
183
|
s.licenses = ["MIT".freeze]
|
|
162
|
-
s.rubygems_version = "3.7.
|
|
184
|
+
s.rubygems_version = "3.7.2".freeze
|
|
163
185
|
s.summary = "Scout essential tools".freeze
|
|
164
186
|
|
|
165
187
|
s.specification_version = 4
|
|
@@ -26,10 +26,9 @@ class TestOpenRemote < Test::Unit::TestCase
|
|
|
26
26
|
teardown
|
|
27
27
|
sss 0
|
|
28
28
|
assert_raises ConcurrentStreamProcessFailed do
|
|
29
|
-
stream = Open.open("ftp://ftp.ncbi.nlm.nih.gov/pub/geo/DATA/SOFT/GDS/GDS3148.soft.gz", nocache: true)
|
|
29
|
+
stream = Open.open("ftp://ftp.ncbi.nlm.nih.gov/pub/geo/DATA/SOFT/GDS/GDS3148.soft.gz", nocache: true, no_fail: false)
|
|
30
30
|
stream.read
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
|
-
|
|
34
33
|
end
|
|
35
34
|
|