rubyn-code 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +620 -0
- data/db/migrations/000_create_schema_migrations.sql +4 -0
- data/db/migrations/001_create_sessions.sql +16 -0
- data/db/migrations/002_create_messages.sql +16 -0
- data/db/migrations/003_create_tasks.sql +17 -0
- data/db/migrations/004_create_task_dependencies.sql +8 -0
- data/db/migrations/005_create_memories.sql +44 -0
- data/db/migrations/006_create_cost_records.sql +16 -0
- data/db/migrations/007_create_hooks.sql +12 -0
- data/db/migrations/008_create_skills_cache.sql +8 -0
- data/db/migrations/009_create_teams.sql +27 -0
- data/db/migrations/010_create_instincts.sql +15 -0
- data/exe/rubyn-code +6 -0
- data/lib/rubyn_code/agent/conversation.rb +193 -0
- data/lib/rubyn_code/agent/loop.rb +517 -0
- data/lib/rubyn_code/agent/loop_detector.rb +78 -0
- data/lib/rubyn_code/auth/oauth.rb +174 -0
- data/lib/rubyn_code/auth/server.rb +126 -0
- data/lib/rubyn_code/auth/token_store.rb +153 -0
- data/lib/rubyn_code/autonomous/daemon.rb +233 -0
- data/lib/rubyn_code/autonomous/idle_poller.rb +111 -0
- data/lib/rubyn_code/autonomous/task_claimer.rb +100 -0
- data/lib/rubyn_code/background/job.rb +19 -0
- data/lib/rubyn_code/background/notifier.rb +44 -0
- data/lib/rubyn_code/background/worker.rb +146 -0
- data/lib/rubyn_code/cli/app.rb +118 -0
- data/lib/rubyn_code/cli/input_handler.rb +79 -0
- data/lib/rubyn_code/cli/renderer.rb +205 -0
- data/lib/rubyn_code/cli/repl.rb +519 -0
- data/lib/rubyn_code/cli/spinner.rb +100 -0
- data/lib/rubyn_code/cli/stream_formatter.rb +149 -0
- data/lib/rubyn_code/config/defaults.rb +43 -0
- data/lib/rubyn_code/config/project_config.rb +120 -0
- data/lib/rubyn_code/config/settings.rb +127 -0
- data/lib/rubyn_code/context/auto_compact.rb +81 -0
- data/lib/rubyn_code/context/compactor.rb +89 -0
- data/lib/rubyn_code/context/manager.rb +91 -0
- data/lib/rubyn_code/context/manual_compact.rb +87 -0
- data/lib/rubyn_code/context/micro_compact.rb +135 -0
- data/lib/rubyn_code/db/connection.rb +176 -0
- data/lib/rubyn_code/db/migrator.rb +146 -0
- data/lib/rubyn_code/db/schema.rb +106 -0
- data/lib/rubyn_code/hooks/built_in.rb +124 -0
- data/lib/rubyn_code/hooks/registry.rb +99 -0
- data/lib/rubyn_code/hooks/runner.rb +88 -0
- data/lib/rubyn_code/hooks/user_hooks.rb +90 -0
- data/lib/rubyn_code/learning/extractor.rb +191 -0
- data/lib/rubyn_code/learning/injector.rb +138 -0
- data/lib/rubyn_code/learning/instinct.rb +172 -0
- data/lib/rubyn_code/llm/client.rb +218 -0
- data/lib/rubyn_code/llm/message_builder.rb +116 -0
- data/lib/rubyn_code/llm/streaming.rb +203 -0
- data/lib/rubyn_code/mcp/client.rb +139 -0
- data/lib/rubyn_code/mcp/config.rb +83 -0
- data/lib/rubyn_code/mcp/sse_transport.rb +225 -0
- data/lib/rubyn_code/mcp/stdio_transport.rb +196 -0
- data/lib/rubyn_code/mcp/tool_bridge.rb +164 -0
- data/lib/rubyn_code/memory/models.rb +62 -0
- data/lib/rubyn_code/memory/search.rb +181 -0
- data/lib/rubyn_code/memory/session_persistence.rb +194 -0
- data/lib/rubyn_code/memory/store.rb +199 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +159 -0
- data/lib/rubyn_code/observability/cost_calculator.rb +61 -0
- data/lib/rubyn_code/observability/models.rb +29 -0
- data/lib/rubyn_code/observability/token_counter.rb +42 -0
- data/lib/rubyn_code/observability/usage_reporter.rb +140 -0
- data/lib/rubyn_code/output/diff_renderer.rb +212 -0
- data/lib/rubyn_code/output/formatter.rb +120 -0
- data/lib/rubyn_code/permissions/deny_list.rb +49 -0
- data/lib/rubyn_code/permissions/policy.rb +59 -0
- data/lib/rubyn_code/permissions/prompter.rb +80 -0
- data/lib/rubyn_code/permissions/tier.rb +22 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +95 -0
- data/lib/rubyn_code/protocols/plan_approval.rb +67 -0
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +109 -0
- data/lib/rubyn_code/skills/catalog.rb +70 -0
- data/lib/rubyn_code/skills/document.rb +80 -0
- data/lib/rubyn_code/skills/loader.rb +57 -0
- data/lib/rubyn_code/sub_agents/runner.rb +168 -0
- data/lib/rubyn_code/sub_agents/summarizer.rb +57 -0
- data/lib/rubyn_code/tasks/dag.rb +208 -0
- data/lib/rubyn_code/tasks/manager.rb +212 -0
- data/lib/rubyn_code/tasks/models.rb +31 -0
- data/lib/rubyn_code/teams/mailbox.rb +128 -0
- data/lib/rubyn_code/teams/manager.rb +175 -0
- data/lib/rubyn_code/teams/teammate.rb +38 -0
- data/lib/rubyn_code/tools/background_run.rb +41 -0
- data/lib/rubyn_code/tools/base.rb +84 -0
- data/lib/rubyn_code/tools/bash.rb +81 -0
- data/lib/rubyn_code/tools/bundle_add.rb +53 -0
- data/lib/rubyn_code/tools/bundle_install.rb +41 -0
- data/lib/rubyn_code/tools/compact.rb +57 -0
- data/lib/rubyn_code/tools/db_migrate.rb +52 -0
- data/lib/rubyn_code/tools/edit_file.rb +49 -0
- data/lib/rubyn_code/tools/executor.rb +62 -0
- data/lib/rubyn_code/tools/git_commit.rb +97 -0
- data/lib/rubyn_code/tools/git_diff.rb +61 -0
- data/lib/rubyn_code/tools/git_log.rb +59 -0
- data/lib/rubyn_code/tools/git_status.rb +59 -0
- data/lib/rubyn_code/tools/glob.rb +44 -0
- data/lib/rubyn_code/tools/grep.rb +81 -0
- data/lib/rubyn_code/tools/load_skill.rb +41 -0
- data/lib/rubyn_code/tools/memory_search.rb +77 -0
- data/lib/rubyn_code/tools/memory_write.rb +52 -0
- data/lib/rubyn_code/tools/rails_generate.rb +54 -0
- data/lib/rubyn_code/tools/read_file.rb +38 -0
- data/lib/rubyn_code/tools/read_inbox.rb +64 -0
- data/lib/rubyn_code/tools/registry.rb +48 -0
- data/lib/rubyn_code/tools/review_pr.rb +145 -0
- data/lib/rubyn_code/tools/run_specs.rb +75 -0
- data/lib/rubyn_code/tools/schema.rb +59 -0
- data/lib/rubyn_code/tools/send_message.rb +53 -0
- data/lib/rubyn_code/tools/spawn_agent.rb +154 -0
- data/lib/rubyn_code/tools/spawn_teammate.rb +168 -0
- data/lib/rubyn_code/tools/task.rb +148 -0
- data/lib/rubyn_code/tools/web_fetch.rb +108 -0
- data/lib/rubyn_code/tools/web_search.rb +196 -0
- data/lib/rubyn_code/tools/write_file.rb +30 -0
- data/lib/rubyn_code/version.rb +5 -0
- data/lib/rubyn_code.rb +203 -0
- data/skills/code_quality/fits_in_your_head.md +189 -0
- data/skills/code_quality/naming_conventions.md +213 -0
- data/skills/code_quality/null_object.md +205 -0
- data/skills/code_quality/technical_debt.md +135 -0
- data/skills/code_quality/value_objects.md +216 -0
- data/skills/code_quality/yagni.md +176 -0
- data/skills/design_patterns/adapter.md +191 -0
- data/skills/design_patterns/bridge_memento_visitor.md +254 -0
- data/skills/design_patterns/builder.md +158 -0
- data/skills/design_patterns/command.md +126 -0
- data/skills/design_patterns/composite.md +147 -0
- data/skills/design_patterns/decorator.md +204 -0
- data/skills/design_patterns/facade.md +133 -0
- data/skills/design_patterns/factory_method.md +169 -0
- data/skills/design_patterns/iterator.md +116 -0
- data/skills/design_patterns/mediator.md +133 -0
- data/skills/design_patterns/observer.md +177 -0
- data/skills/design_patterns/proxy.md +140 -0
- data/skills/design_patterns/singleton.md +124 -0
- data/skills/design_patterns/state.md +207 -0
- data/skills/design_patterns/strategy.md +127 -0
- data/skills/design_patterns/template_method.md +173 -0
- data/skills/gems/devise.md +365 -0
- data/skills/gems/dry_rb.md +186 -0
- data/skills/gems/factory_bot.md +268 -0
- data/skills/gems/faraday.md +263 -0
- data/skills/gems/graphql_ruby.md +514 -0
- data/skills/gems/pundit.md +446 -0
- data/skills/gems/redis.md +219 -0
- data/skills/gems/rubocop.md +257 -0
- data/skills/gems/sidekiq.md +360 -0
- data/skills/gems/stripe.md +224 -0
- data/skills/minitest/assertions.md +185 -0
- data/skills/minitest/fixtures.md +238 -0
- data/skills/minitest/integration_tests.md +210 -0
- data/skills/minitest/mailers_and_jobs.md +218 -0
- data/skills/minitest/mocking_stubbing.md +202 -0
- data/skills/minitest/service_tests_and_performance.md +246 -0
- data/skills/minitest/structure_and_conventions.md +169 -0
- data/skills/minitest/system_tests.md +237 -0
- data/skills/rails/action_cable.md +160 -0
- data/skills/rails/active_record_basics.md +174 -0
- data/skills/rails/active_storage.md +242 -0
- data/skills/rails/api_design.md +212 -0
- data/skills/rails/associations.md +182 -0
- data/skills/rails/background_jobs.md +212 -0
- data/skills/rails/caching.md +158 -0
- data/skills/rails/callbacks.md +135 -0
- data/skills/rails/concerns_controllers.md +218 -0
- data/skills/rails/concerns_models.md +280 -0
- data/skills/rails/controllers.md +190 -0
- data/skills/rails/engines.md +201 -0
- data/skills/rails/form_objects.md +168 -0
- data/skills/rails/hotwire.md +229 -0
- data/skills/rails/internationalization.md +192 -0
- data/skills/rails/logging.md +198 -0
- data/skills/rails/mailers.md +180 -0
- data/skills/rails/migrations.md +200 -0
- data/skills/rails/multitenancy.md +207 -0
- data/skills/rails/n_plus_one.md +151 -0
- data/skills/rails/presenters.md +244 -0
- data/skills/rails/query_objects.md +177 -0
- data/skills/rails/routing.md +194 -0
- data/skills/rails/scopes.md +187 -0
- data/skills/rails/security.md +233 -0
- data/skills/rails/serializers.md +243 -0
- data/skills/rails/service_objects.md +184 -0
- data/skills/rails/testing_strategy.md +258 -0
- data/skills/rails/validations.md +206 -0
- data/skills/refactoring/code_smells.md +251 -0
- data/skills/refactoring/command_query_separation.md +166 -0
- data/skills/refactoring/encapsulate_collection.md +125 -0
- data/skills/refactoring/extract_class.md +138 -0
- data/skills/refactoring/extract_method.md +185 -0
- data/skills/refactoring/replace_conditional.md +211 -0
- data/skills/refactoring/value_objects.md +246 -0
- data/skills/rspec/build_stubbed.md +199 -0
- data/skills/rspec/factory_design.md +206 -0
- data/skills/rspec/let_vs_let_bang.md +161 -0
- data/skills/rspec/mocking_stubbing.md +209 -0
- data/skills/rspec/request_specs.md +212 -0
- data/skills/rspec/service_specs.md +262 -0
- data/skills/rspec/shared_examples.md +244 -0
- data/skills/rspec/system_specs.md +286 -0
- data/skills/rspec/test_performance.md +215 -0
- data/skills/ruby/blocks_procs_lambdas.md +204 -0
- data/skills/ruby/classes.md +155 -0
- data/skills/ruby/concurrency.md +194 -0
- data/skills/ruby/data_struct_openstruct.md +158 -0
- data/skills/ruby/debugging_profiling.md +204 -0
- data/skills/ruby/enumerable_patterns.md +168 -0
- data/skills/ruby/exception_handling.md +199 -0
- data/skills/ruby/file_io.md +217 -0
- data/skills/ruby/hashes.md +195 -0
- data/skills/ruby/metaprogramming.md +170 -0
- data/skills/ruby/modules.md +210 -0
- data/skills/ruby/pattern_matching.md +177 -0
- data/skills/ruby/regular_expressions.md +166 -0
- data/skills/ruby/result_objects.md +200 -0
- data/skills/ruby/strings.md +177 -0
- data/skills/ruby_project/bundler_dependencies.md +181 -0
- data/skills/ruby_project/cli_tools.md +224 -0
- data/skills/ruby_project/rake_tasks.md +146 -0
- data/skills/ruby_project/structure.md +261 -0
- data/skills/sinatra/application_structure.md +241 -0
- data/skills/sinatra/middleware_and_deployment.md +221 -0
- data/skills/sinatra/testing.md +233 -0
- data/skills/solid/dependency_inversion.md +195 -0
- data/skills/solid/interface_segregation.md +237 -0
- data/skills/solid/liskov_substitution.md +263 -0
- data/skills/solid/open_closed.md +212 -0
- data/skills/solid/single_responsibility.md +183 -0
- metadata +397 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Ruby: Concurrency
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Ruby has multiple concurrency primitives: Threads for I/O parallelism, Fibers for cooperative concurrency, and Ractors (Ruby 3+) for true parallelism. Choose the right tool for the workload.
|
|
6
|
+
|
|
7
|
+
### Threads — Best for I/O-Bound Work
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Parallel HTTP requests — threads shine here because each waits on I/O
|
|
11
|
+
urls = %w[
|
|
12
|
+
https://api.example.com/orders
|
|
13
|
+
https://api.example.com/users
|
|
14
|
+
https://api.example.com/products
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
results = urls.map do |url|
|
|
18
|
+
Thread.new(url) do |u|
|
|
19
|
+
Faraday.get(u).body
|
|
20
|
+
end
|
|
21
|
+
end.map(&:value) # .value blocks until the thread finishes
|
|
22
|
+
|
|
23
|
+
# Thread pool for controlled concurrency
|
|
24
|
+
require "concurrent-ruby"
|
|
25
|
+
|
|
26
|
+
pool = Concurrent::FixedThreadPool.new(5)
|
|
27
|
+
futures = urls.map do |url|
|
|
28
|
+
Concurrent::Future.execute(executor: pool) do
|
|
29
|
+
Faraday.get(url).body
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
results = futures.map(&:value)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# Thread-safe shared state with Mutex
|
|
37
|
+
class Counter
|
|
38
|
+
def initialize
|
|
39
|
+
@count = 0
|
|
40
|
+
@mutex = Mutex.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def increment
|
|
44
|
+
@mutex.synchronize { @count += 1 }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def value
|
|
48
|
+
@mutex.synchronize { @count }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
counter = Counter.new
|
|
53
|
+
threads = 10.times.map do
|
|
54
|
+
Thread.new { 1000.times { counter.increment } }
|
|
55
|
+
end
|
|
56
|
+
threads.each(&:join)
|
|
57
|
+
counter.value # => 10000 (always correct with Mutex)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### concurrent-ruby — Production-Grade Concurrency
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
# Gemfile
|
|
64
|
+
gem "concurrent-ruby"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# Thread-safe data structures
|
|
69
|
+
require "concurrent"
|
|
70
|
+
|
|
71
|
+
# Atomic values — no Mutex needed
|
|
72
|
+
counter = Concurrent::AtomicFixnum.new(0)
|
|
73
|
+
counter.increment
|
|
74
|
+
counter.value # => 1
|
|
75
|
+
|
|
76
|
+
# Thread-safe hash
|
|
77
|
+
cache = Concurrent::Map.new
|
|
78
|
+
cache["key"] = "value"
|
|
79
|
+
cache.fetch_or_store("key") { expensive_computation }
|
|
80
|
+
|
|
81
|
+
# Promises for async pipelines
|
|
82
|
+
result = Concurrent::Promise.fulfill("data")
|
|
83
|
+
.then { |data| transform(data) }
|
|
84
|
+
.then { |transformed| save(transformed) }
|
|
85
|
+
.rescue { |error| handle_error(error) }
|
|
86
|
+
.value # Blocks until chain completes
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Fibers — Cooperative Concurrency
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# Fibers yield control explicitly — useful for generators and coroutines
|
|
93
|
+
def id_generator
|
|
94
|
+
Fiber.new do
|
|
95
|
+
id = 0
|
|
96
|
+
loop do
|
|
97
|
+
Fiber.yield(id += 1)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
gen = id_generator
|
|
103
|
+
gen.resume # => 1
|
|
104
|
+
gen.resume # => 2
|
|
105
|
+
gen.resume # => 3
|
|
106
|
+
|
|
107
|
+
# Enumerator (built on Fibers) for lazy sequences
|
|
108
|
+
def fibonacci
|
|
109
|
+
Enumerator.new do |y|
|
|
110
|
+
a, b = 0, 1
|
|
111
|
+
loop do
|
|
112
|
+
y.yield a
|
|
113
|
+
a, b = b, a + b
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
fibonacci.lazy.select(&:odd?).first(10)
|
|
119
|
+
# => [1, 1, 3, 5, 13, 21, 55, 89, 233, 377]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Batch Processing with `in_batches` + Threads
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# Process large datasets with controlled parallelism
|
|
126
|
+
class BatchProcessor
|
|
127
|
+
def initialize(concurrency: 4)
|
|
128
|
+
@pool = Concurrent::FixedThreadPool.new(concurrency)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def process(scope, batch_size: 100, &block)
|
|
132
|
+
futures = []
|
|
133
|
+
|
|
134
|
+
scope.find_in_batches(batch_size: batch_size) do |batch|
|
|
135
|
+
futures << Concurrent::Future.execute(executor: @pool) do
|
|
136
|
+
batch.each { |record| block.call(record) }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
futures.each(&:value!) # Wait for all, re-raise exceptions
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def shutdown
|
|
144
|
+
@pool.shutdown
|
|
145
|
+
@pool.wait_for_termination(30)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Usage
|
|
150
|
+
processor = BatchProcessor.new(concurrency: 4)
|
|
151
|
+
processor.process(Order.where(status: :pending)) do |order|
|
|
152
|
+
Orders::ProcessService.call(order)
|
|
153
|
+
end
|
|
154
|
+
processor.shutdown
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Why This Is Good
|
|
158
|
+
|
|
159
|
+
- **Threads for I/O.** 10 parallel HTTP requests complete in the time of 1 sequential request. Ruby's GVL releases during I/O, enabling true parallelism for network-bound work.
|
|
160
|
+
- **`concurrent-ruby` is production-tested.** Thread pools, atomic values, promises, and thread-safe collections — battle-hardened by millions of Ruby apps.
|
|
161
|
+
- **Mutex for correctness.** Shared mutable state without a Mutex causes race conditions. With a Mutex, operations are atomic and predictable.
|
|
162
|
+
- **Fibers for generators.** Infinite sequences, lazy evaluation, and cooperative multitasking without threads.
|
|
163
|
+
|
|
164
|
+
## Anti-Pattern
|
|
165
|
+
|
|
166
|
+
Unprotected shared state or over-threading:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# BAD: Race condition — no synchronization
|
|
170
|
+
results = []
|
|
171
|
+
threads = urls.map do |url|
|
|
172
|
+
Thread.new { results << Faraday.get(url).body } # Array#<< is NOT thread-safe
|
|
173
|
+
end
|
|
174
|
+
threads.each(&:join)
|
|
175
|
+
# results may be corrupted, missing items, or raise errors
|
|
176
|
+
|
|
177
|
+
# FIX: Use thread-safe collection or collect from thread return values
|
|
178
|
+
results = urls.map do |url|
|
|
179
|
+
Thread.new { Faraday.get(url).body }
|
|
180
|
+
end.map(&:value)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## When To Apply
|
|
184
|
+
|
|
185
|
+
- **I/O-bound work** — HTTP requests, file reads, database queries across multiple connections. Threads provide real speedup.
|
|
186
|
+
- **Background processing** — Use `concurrent-ruby` thread pools for in-process parallelism.
|
|
187
|
+
- **Lazy sequences** — Fibers and Enumerators for infinite or expensive sequences that are consumed incrementally.
|
|
188
|
+
|
|
189
|
+
## When NOT To Apply
|
|
190
|
+
|
|
191
|
+
- **CPU-bound work in MRI Ruby.** The Global VM Lock (GVL) prevents true parallel computation. Threads won't speed up math or data processing. Use Ractors or a separate process (via `parallel` gem).
|
|
192
|
+
- **Simple sequential code.** If the operation takes 50ms, threading adds overhead without meaningful speedup.
|
|
193
|
+
- **Rails request handling.** Puma already manages threads for you. Don't create threads inside controller actions — use background jobs instead.
|
|
194
|
+
- **Avoid more than 10-20 threads.** Thread creation and context switching have overhead. Use a fixed thread pool, not unbounded `Thread.new`.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Ruby: Data, Struct, and OpenStruct
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Ruby provides three built-in ways to create simple data-holding classes. Choose the right one based on mutability needs and Ruby version.
|
|
6
|
+
|
|
7
|
+
### Data (Ruby 3.2+) — Immutable Value Objects
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Data.define creates a frozen, immutable value class
|
|
11
|
+
Point = Data.define(:x, :y)
|
|
12
|
+
Money = Data.define(:cents, :currency)
|
|
13
|
+
DateRange = Data.define(:start_date, :end_date)
|
|
14
|
+
|
|
15
|
+
# Creation
|
|
16
|
+
point = Point.new(x: 10, y: 20)
|
|
17
|
+
price = Money.new(cents: 19_99, currency: "USD")
|
|
18
|
+
|
|
19
|
+
# Immutable — frozen by default
|
|
20
|
+
point.x # => 10
|
|
21
|
+
point.frozen? # => true
|
|
22
|
+
point.x = 5 # => FrozenError
|
|
23
|
+
|
|
24
|
+
# Equality by value
|
|
25
|
+
Point.new(x: 1, y: 2) == Point.new(x: 1, y: 2) # => true
|
|
26
|
+
|
|
27
|
+
# Pattern matching
|
|
28
|
+
case price
|
|
29
|
+
in Money[cents: (0..99), currency: "USD"]
|
|
30
|
+
"Under a dollar"
|
|
31
|
+
in Money[cents: (100..), currency: "USD"]
|
|
32
|
+
"A dollar or more"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Add behavior with a block
|
|
36
|
+
Money = Data.define(:cents, :currency) do
|
|
37
|
+
def to_s
|
|
38
|
+
"$#{format('%.2f', cents / 100.0)}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def +(other)
|
|
42
|
+
raise ArgumentError, "Currency mismatch" unless currency == other.currency
|
|
43
|
+
self.class.new(cents: cents + other.cents, currency: currency)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.zero(currency = "USD")
|
|
47
|
+
new(cents: 0, currency: currency)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
price = Money.new(cents: 10_00, currency: "USD")
|
|
52
|
+
tax = Money.new(cents: 80, currency: "USD")
|
|
53
|
+
total = price + tax
|
|
54
|
+
total.to_s # => "$10.80"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Struct — Mutable Data Containers
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# Struct creates a mutable class with attribute accessors
|
|
61
|
+
OrderSummary = Struct.new(:reference, :total, :status, keyword_init: true)
|
|
62
|
+
|
|
63
|
+
summary = OrderSummary.new(reference: "ORD-001", total: 50_00, status: "pending")
|
|
64
|
+
summary.reference # => "ORD-001"
|
|
65
|
+
summary.status = "shipped" # Mutable — can change
|
|
66
|
+
|
|
67
|
+
# Struct supports Enumerable
|
|
68
|
+
summary.to_a # => ["ORD-001", 50_00, "shipped"]
|
|
69
|
+
summary.to_h # => { reference: "ORD-001", total: 50_00, status: "shipped" }
|
|
70
|
+
|
|
71
|
+
# Add methods
|
|
72
|
+
Result = Struct.new(:success, :value, :error, keyword_init: true) do
|
|
73
|
+
def success?
|
|
74
|
+
success == true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def failure?
|
|
78
|
+
!success?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
result = Result.new(success: true, value: order, error: nil)
|
|
83
|
+
result.success? # => true
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### OpenStruct — Dynamic Attributes (Use Sparingly)
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# OpenStruct allows any attribute — no predefined structure
|
|
90
|
+
config = OpenStruct.new(api_key: "sk-123", timeout: 30)
|
|
91
|
+
config.api_key # => "sk-123"
|
|
92
|
+
config.new_field = "added dynamically" # Any attribute, any time
|
|
93
|
+
config.new_field # => "added dynamically"
|
|
94
|
+
|
|
95
|
+
# OpenStruct is SLOW — uses method_missing internally
|
|
96
|
+
# ~10x slower than Struct for attribute access
|
|
97
|
+
# ~100x slower than a plain class
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Decision Tree
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Do you need a simple data container?
|
|
104
|
+
├── Is the data immutable (value object)?
|
|
105
|
+
│ ├── Ruby 3.2+? → Data.define
|
|
106
|
+
│ └── Ruby < 3.2? → Struct.new with .freeze
|
|
107
|
+
├── Is the data mutable?
|
|
108
|
+
│ └── Struct.new (keyword_init: true)
|
|
109
|
+
├── Are attributes dynamic/unknown at design time?
|
|
110
|
+
│ └── OpenStruct (but consider a Hash instead)
|
|
111
|
+
└── Does the object need complex behavior?
|
|
112
|
+
└── Write a full class
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## When To Apply
|
|
116
|
+
|
|
117
|
+
- **`Data.define`** for value objects: Money, Email, Coordinates, API responses, Result types. Anywhere immutability and value equality matter.
|
|
118
|
+
- **`Struct`** for simple data transfer objects: search results, service return types, configuration that's built step by step.
|
|
119
|
+
- **OpenStruct** for quick prototyping only. Replace with Struct or Data before code review.
|
|
120
|
+
|
|
121
|
+
## When NOT To Apply
|
|
122
|
+
|
|
123
|
+
- **ActiveRecord models.** They have their own attribute system. Don't wrap them in Struct/Data.
|
|
124
|
+
- **Complex domain objects.** If the object has 5+ methods of behavior (not just accessors), write a class.
|
|
125
|
+
- **Performance-critical paths.** OpenStruct is slow. In hot loops, use Struct or plain classes.
|
|
126
|
+
- **OpenStruct in production code.** Its dynamic nature makes typos silent (`config.api_ky = "..."` creates a new attribute instead of raising). Struct catches this at construction time.
|
|
127
|
+
|
|
128
|
+
## Edge Cases
|
|
129
|
+
|
|
130
|
+
**Struct as a base class:**
|
|
131
|
+
```ruby
|
|
132
|
+
class User < Struct.new(:name, :email, keyword_init: true)
|
|
133
|
+
def greeting
|
|
134
|
+
"Hello, #{name}!"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
This works but is considered unusual. Prefer `Data.define` with a block or a plain class.
|
|
139
|
+
|
|
140
|
+
**Freezing a Struct for immutability (pre-Ruby 3.2):**
|
|
141
|
+
```ruby
|
|
142
|
+
Config = Struct.new(:api_key, :timeout, keyword_init: true)
|
|
143
|
+
config = Config.new(api_key: "sk-123", timeout: 30).freeze
|
|
144
|
+
config.api_key = "new" # => FrozenError
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Nested Data objects:**
|
|
148
|
+
```ruby
|
|
149
|
+
Address = Data.define(:street, :city, :state, :zip)
|
|
150
|
+
Customer = Data.define(:name, :email, :address)
|
|
151
|
+
|
|
152
|
+
customer = Customer.new(
|
|
153
|
+
name: "Alice",
|
|
154
|
+
email: "alice@example.com",
|
|
155
|
+
address: Address.new(street: "123 Main", city: "Austin", state: "TX", zip: "78701")
|
|
156
|
+
)
|
|
157
|
+
customer.address.city # => "Austin"
|
|
158
|
+
```
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Ruby: Debugging and Profiling
|
|
2
|
+
|
|
3
|
+
## Pattern
|
|
4
|
+
|
|
5
|
+
Use the right debugging tool for the problem: `debug` gem (Ruby 3.1+ built-in) for interactive debugging, logging for production visibility, and profiling tools to find performance bottlenecks.
|
|
6
|
+
|
|
7
|
+
### Interactive Debugging (debug gem)
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Ruby 3.1+ has `debug` built in — no gem needed
|
|
11
|
+
# Add a breakpoint anywhere:
|
|
12
|
+
def create_order(params)
|
|
13
|
+
order = Order.new(params)
|
|
14
|
+
binding.break # Execution pauses here — inspect variables, step through code
|
|
15
|
+
order.save!
|
|
16
|
+
order
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Or use the shorter form
|
|
20
|
+
def process(data)
|
|
21
|
+
debugger # Same as binding.break
|
|
22
|
+
transform(data)
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Debug session commands:
|
|
27
|
+
```
|
|
28
|
+
# In the debug console:
|
|
29
|
+
(rdbg) p order # Print variable
|
|
30
|
+
(rdbg) pp order.errors # Pretty-print
|
|
31
|
+
(rdbg) n # Next line (step over)
|
|
32
|
+
(rdbg) s # Step into method
|
|
33
|
+
(rdbg) c # Continue execution
|
|
34
|
+
(rdbg) info locals # Show all local variables
|
|
35
|
+
(rdbg) bt # Backtrace
|
|
36
|
+
(rdbg) watch @total # Break when @total changes
|
|
37
|
+
(rdbg) break Order#save # Break when Order#save is called
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Pry (Popular Alternative)
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# Gemfile
|
|
44
|
+
gem "pry", group: [:development, :test]
|
|
45
|
+
gem "pry-byebug", group: [:development, :test] # Adds step/next/continue
|
|
46
|
+
|
|
47
|
+
# Usage
|
|
48
|
+
def calculate_total(items)
|
|
49
|
+
subtotal = items.sum(&:price)
|
|
50
|
+
binding.pry # Drops into Pry REPL
|
|
51
|
+
subtotal * 1.08
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Pry commands:
|
|
55
|
+
# ls object — list methods
|
|
56
|
+
# cd object — change context into object
|
|
57
|
+
# show-method — show source code of a method
|
|
58
|
+
# whereami — show current location
|
|
59
|
+
# next/step/continue — navigation (with pry-byebug)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Logging Best Practices
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# Rails logger levels: debug < info < warn < error < fatal
|
|
66
|
+
class Orders::CreateService
|
|
67
|
+
def call(params, user)
|
|
68
|
+
Rails.logger.info("[Orders::CreateService] Starting for user=#{user.id}")
|
|
69
|
+
|
|
70
|
+
order = user.orders.build(params)
|
|
71
|
+
unless order.valid?
|
|
72
|
+
Rails.logger.warn("[Orders::CreateService] Validation failed: #{order.errors.full_messages}")
|
|
73
|
+
return Result.failure(order.errors)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
order.save!
|
|
77
|
+
Rails.logger.info("[Orders::CreateService] Created order=#{order.id} total=#{order.total}")
|
|
78
|
+
|
|
79
|
+
Result.success(order)
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
Rails.logger.error("[Orders::CreateService] Failed: #{e.class}: #{e.message}")
|
|
82
|
+
Rails.logger.debug(e.backtrace.first(10).join("\n"))
|
|
83
|
+
raise
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Tagged logging — adds context to every log line
|
|
88
|
+
Rails.logger = ActiveSupport::TaggedLogging.new(Logger.new($stdout))
|
|
89
|
+
Rails.logger.tagged("OrderService", "user:#{user.id}") do
|
|
90
|
+
Rails.logger.info("Processing order")
|
|
91
|
+
# => [OrderService] [user:42] Processing order
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Structured logging for production (JSON)
|
|
95
|
+
# Gemfile
|
|
96
|
+
gem "lograge"
|
|
97
|
+
|
|
98
|
+
# config/environments/production.rb
|
|
99
|
+
config.lograge.enabled = true
|
|
100
|
+
config.lograge.formatter = Lograge::Formatters::Json.new
|
|
101
|
+
config.lograge.custom_payload do |controller|
|
|
102
|
+
{ user_id: controller.current_user&.id }
|
|
103
|
+
end
|
|
104
|
+
# Output: {"method":"POST","path":"/orders","status":201,"duration":45.2,"user_id":42}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Performance Profiling
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# Benchmark a block
|
|
111
|
+
require "benchmark"
|
|
112
|
+
|
|
113
|
+
time = Benchmark.measure do
|
|
114
|
+
Order.where(status: :pending).find_each { |o| process(o) }
|
|
115
|
+
end
|
|
116
|
+
puts time # => 0.120000 0.030000 0.150000 ( 0.152345)
|
|
117
|
+
|
|
118
|
+
# Compare approaches
|
|
119
|
+
Benchmark.bm(20) do |x|
|
|
120
|
+
x.report("find_each:") { Order.pending.find_each { |o| o.total } }
|
|
121
|
+
x.report("pluck:") { Order.pending.pluck(:total) }
|
|
122
|
+
x.report("in_batches:") { Order.pending.in_batches.each_record { |o| o.total } }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Memory profiling
|
|
126
|
+
# Gemfile
|
|
127
|
+
gem "memory_profiler", group: :development
|
|
128
|
+
|
|
129
|
+
report = MemoryProfiler.report do
|
|
130
|
+
users = User.all.to_a
|
|
131
|
+
users.map(&:email)
|
|
132
|
+
end
|
|
133
|
+
report.pretty_print
|
|
134
|
+
# Shows: allocated memory, retained memory, allocation by gem/file/location
|
|
135
|
+
|
|
136
|
+
# rack-mini-profiler for web requests
|
|
137
|
+
# Gemfile
|
|
138
|
+
gem "rack-mini-profiler", group: :development
|
|
139
|
+
|
|
140
|
+
# Shows a speed badge on every page with:
|
|
141
|
+
# - Total request time
|
|
142
|
+
# - SQL query count and time
|
|
143
|
+
# - Memory usage
|
|
144
|
+
# - Flamegraph link
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Finding N+1 Queries
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# Bullet gem detects N+1 in development
|
|
151
|
+
# Gemfile
|
|
152
|
+
gem "bullet", group: :development
|
|
153
|
+
|
|
154
|
+
# config/environments/development.rb
|
|
155
|
+
config.after_initialize do
|
|
156
|
+
Bullet.enable = true
|
|
157
|
+
Bullet.alert = true # Browser popup
|
|
158
|
+
Bullet.rails_logger = true # Log to Rails log
|
|
159
|
+
Bullet.add_footer = true # Badge in page footer
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# strict_loading (Rails 6.1+) — raises on lazy loading
|
|
163
|
+
class Order < ApplicationRecord
|
|
164
|
+
self.strict_loading_by_default = true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Or per-query
|
|
168
|
+
Order.strict_loading.includes(:line_items).each do |order|
|
|
169
|
+
order.line_items # Works — preloaded
|
|
170
|
+
order.user # Raises! Not preloaded
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Production Debugging
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
# Rails console in production
|
|
178
|
+
# RAILS_ENV=production rails console
|
|
179
|
+
|
|
180
|
+
# Safe read-only queries
|
|
181
|
+
ActiveRecord::Base.connected_to(role: :reading) do
|
|
182
|
+
Order.where(status: :pending).count
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Sandbox mode — rolls back all changes on exit
|
|
186
|
+
# rails console --sandbox
|
|
187
|
+
|
|
188
|
+
# Quick diagnostics
|
|
189
|
+
Rails.logger.level = :debug # Temporarily increase verbosity
|
|
190
|
+
ActiveRecord::Base.logger = Logger.new($stdout) # See all SQL
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## When To Apply
|
|
194
|
+
|
|
195
|
+
- **`debugger`/`binding.pry` for investigation.** When you don't understand why code behaves a certain way, stop execution and inspect state.
|
|
196
|
+
- **Logging for production.** Always log: service entry/exit, errors with context, and performance metrics. Never log: passwords, API keys, or PII.
|
|
197
|
+
- **Profiling before optimizing.** Measure first. The bottleneck is almost never where you think it is.
|
|
198
|
+
- **Bullet/strict_loading in development.** Catch N+1s before they reach production.
|
|
199
|
+
|
|
200
|
+
## When NOT To Apply
|
|
201
|
+
|
|
202
|
+
- **Don't leave `debugger` calls in committed code.** Use `binding.pry` and `debugger` for local debugging only. CI should catch any that slip through.
|
|
203
|
+
- **Don't log everything.** Debug-level logging for every method call creates noise. Log at the right level: info for normal flow, warn for recoverable issues, error for failures.
|
|
204
|
+
- **Don't optimize without profiling.** "I think this is slow" → profile it → optimize the actual bottleneck.
|