fractor 0.1.4 → 0.1.6
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/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
- data/.rubocop.yml +14 -8
- data/.rubocop_todo.yml +162 -46
- data/README.adoc +1364 -376
- data/examples/auto_detection/auto_detection.rb +9 -9
- data/examples/continuous_chat_common/message_protocol.rb +53 -0
- data/examples/continuous_chat_fractor/README.adoc +217 -0
- data/examples/continuous_chat_fractor/chat_client.rb +303 -0
- data/examples/continuous_chat_fractor/chat_common.rb +83 -0
- data/examples/continuous_chat_fractor/chat_server.rb +167 -0
- data/examples/continuous_chat_fractor/simulate.rb +345 -0
- data/examples/continuous_chat_server/README.adoc +135 -0
- data/examples/continuous_chat_server/chat_client.rb +303 -0
- data/examples/continuous_chat_server/chat_server.rb +359 -0
- data/examples/continuous_chat_server/simulate.rb +343 -0
- data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
- data/examples/multi_work_type/multi_work_type.rb +30 -29
- data/examples/pipeline_processing/pipeline_processing.rb +15 -15
- data/examples/producer_subscriber/producer_subscriber.rb +20 -16
- data/examples/scatter_gather/scatter_gather.rb +29 -28
- data/examples/simple/sample.rb +5 -5
- data/examples/specialized_workers/specialized_workers.rb +44 -37
- data/lib/fractor/continuous_server.rb +188 -0
- data/lib/fractor/result_aggregator.rb +1 -1
- data/lib/fractor/supervisor.rb +277 -104
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work_queue.rb +68 -0
- data/lib/fractor/work_result.rb +1 -1
- data/lib/fractor/worker.rb +2 -1
- data/lib/fractor/wrapped_ractor.rb +12 -2
- data/lib/fractor.rb +2 -0
- metadata +15 -2
data/examples/simple/sample.rb
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
#
|
|
33
33
|
# =============================================================================
|
|
34
34
|
|
|
35
|
-
require_relative
|
|
35
|
+
require_relative '../../lib/fractor'
|
|
36
36
|
|
|
37
37
|
# Client-specific work item implementation inheriting from Fractor::Work
|
|
38
38
|
class MyWork < Fractor::Work
|
|
@@ -72,14 +72,14 @@ class MyWorker < Fractor::Worker
|
|
|
72
72
|
# It should return a Fractor::WorkResult object
|
|
73
73
|
def process(work)
|
|
74
74
|
# Only print debug information if FRACTOR_DEBUG is enabled
|
|
75
|
-
puts "Working on '#{work.inspect}'" if ENV[
|
|
75
|
+
puts "Working on '#{work.inspect}'" if ENV['FRACTOR_DEBUG']
|
|
76
76
|
|
|
77
77
|
# Check work type and handle accordingly
|
|
78
78
|
if work.is_a?(MyWork)
|
|
79
79
|
if work.value == 5
|
|
80
80
|
# Return a Fractor::WorkResult for errors
|
|
81
81
|
# Store the error object, not just the string
|
|
82
|
-
error = StandardError.new(
|
|
82
|
+
error = StandardError.new('Cannot process value 5')
|
|
83
83
|
return Fractor::WorkResult.new(error: error, work: work)
|
|
84
84
|
end
|
|
85
85
|
|
|
@@ -116,8 +116,8 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
116
116
|
# Run the supervisor to start processing work
|
|
117
117
|
supervisor.run
|
|
118
118
|
|
|
119
|
-
puts
|
|
120
|
-
puts
|
|
119
|
+
puts 'Processing complete.'
|
|
120
|
+
puts 'Final Aggregated Results:'
|
|
121
121
|
# Access the results aggregator from the supervisor
|
|
122
122
|
puts supervisor.results.inspect
|
|
123
123
|
|
|
@@ -9,7 +9,7 @@ module SpecializedWorkers
|
|
|
9
9
|
super({
|
|
10
10
|
data: data,
|
|
11
11
|
operation: operation,
|
|
12
|
-
parameters: parameters
|
|
12
|
+
parameters: parameters,
|
|
13
13
|
})
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -32,12 +32,13 @@ module SpecializedWorkers
|
|
|
32
32
|
|
|
33
33
|
# Second work type: Database operations
|
|
34
34
|
class DatabaseWork < Fractor::Work
|
|
35
|
-
def initialize(data = "", query_type = :select, table = "unknown",
|
|
35
|
+
def initialize(data = "", query_type = :select, table = "unknown",
|
|
36
|
+
conditions = {})
|
|
36
37
|
super({
|
|
37
38
|
data: data,
|
|
38
39
|
query_type: query_type,
|
|
39
40
|
table: table,
|
|
40
|
-
conditions: conditions
|
|
41
|
+
conditions: conditions,
|
|
41
42
|
})
|
|
42
43
|
end
|
|
43
44
|
|
|
@@ -74,14 +75,16 @@ module SpecializedWorkers
|
|
|
74
75
|
unless work.is_a?(ComputeWork)
|
|
75
76
|
return Fractor::WorkResult.new(
|
|
76
77
|
error: "ComputeWorker can only process ComputeWork, got: #{work.class}",
|
|
77
|
-
work: work
|
|
78
|
+
work: work,
|
|
78
79
|
)
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
# Process based on the requested operation
|
|
82
83
|
result = case work.operation
|
|
83
|
-
when :matrix_multiply then matrix_multiply(work.data,
|
|
84
|
-
|
|
84
|
+
when :matrix_multiply then matrix_multiply(work.data,
|
|
85
|
+
work.parameters)
|
|
86
|
+
when :image_transform then image_transform(work.data,
|
|
87
|
+
work.parameters)
|
|
85
88
|
when :path_finding then path_finding(work.data, work.parameters)
|
|
86
89
|
else default_computation(work.data, work.parameters)
|
|
87
90
|
end
|
|
@@ -90,9 +93,9 @@ module SpecializedWorkers
|
|
|
90
93
|
result: {
|
|
91
94
|
operation: work.operation,
|
|
92
95
|
computation_result: result,
|
|
93
|
-
resources_used: @compute_resources
|
|
96
|
+
resources_used: @compute_resources,
|
|
94
97
|
},
|
|
95
|
-
work: work
|
|
98
|
+
work: work,
|
|
96
99
|
)
|
|
97
100
|
end
|
|
98
101
|
|
|
@@ -110,7 +113,7 @@ module SpecializedWorkers
|
|
|
110
113
|
# Simulate image transformation
|
|
111
114
|
sleep(rand(0.1..0.3))
|
|
112
115
|
transforms = params[:transforms] || %i[rotate scale]
|
|
113
|
-
"Image transformation applied: #{transforms.join(
|
|
116
|
+
"Image transformation applied: #{transforms.join(', ')} with parameters #{params}"
|
|
114
117
|
end
|
|
115
118
|
|
|
116
119
|
def path_finding(_data, params)
|
|
@@ -140,7 +143,7 @@ module SpecializedWorkers
|
|
|
140
143
|
unless work.is_a?(DatabaseWork)
|
|
141
144
|
return Fractor::WorkResult.new(
|
|
142
145
|
error: "DatabaseWorker can only process DatabaseWork, got: #{work.class}",
|
|
143
|
-
work: work
|
|
146
|
+
work: work,
|
|
144
147
|
)
|
|
145
148
|
end
|
|
146
149
|
|
|
@@ -148,7 +151,8 @@ module SpecializedWorkers
|
|
|
148
151
|
result = case work.query_type
|
|
149
152
|
when :select then perform_select(work.table, work.conditions)
|
|
150
153
|
when :insert then perform_insert(work.table, work.data)
|
|
151
|
-
when :update then perform_update(work.table, work.data,
|
|
154
|
+
when :update then perform_update(work.table, work.data,
|
|
155
|
+
work.conditions)
|
|
152
156
|
when :delete then perform_delete(work.table, work.conditions)
|
|
153
157
|
else default_query(work.query_type, work.table, work.conditions)
|
|
154
158
|
end
|
|
@@ -159,9 +163,9 @@ module SpecializedWorkers
|
|
|
159
163
|
table: work.table,
|
|
160
164
|
rows_affected: result[:rows_affected],
|
|
161
165
|
data: result[:data],
|
|
162
|
-
execution_time: result[:time]
|
|
166
|
+
execution_time: result[:time],
|
|
163
167
|
},
|
|
164
|
-
work: work
|
|
168
|
+
work: work,
|
|
165
169
|
)
|
|
166
170
|
end
|
|
167
171
|
|
|
@@ -174,8 +178,10 @@ module SpecializedWorkers
|
|
|
174
178
|
record_count = rand(0..20)
|
|
175
179
|
{
|
|
176
180
|
rows_affected: record_count,
|
|
177
|
-
data: record_count
|
|
178
|
-
|
|
181
|
+
data: Array.new(record_count) do |i|
|
|
182
|
+
{ id: i + 1, name: "Record #{i + 1}" }
|
|
183
|
+
end,
|
|
184
|
+
time: rand(0.01..0.05),
|
|
179
185
|
}
|
|
180
186
|
end
|
|
181
187
|
|
|
@@ -185,7 +191,7 @@ module SpecializedWorkers
|
|
|
185
191
|
{
|
|
186
192
|
rows_affected: 1,
|
|
187
193
|
data: { id: rand(1000..9999) },
|
|
188
|
-
time: rand(0.01..0.03)
|
|
194
|
+
time: rand(0.01..0.03),
|
|
189
195
|
}
|
|
190
196
|
end
|
|
191
197
|
|
|
@@ -196,7 +202,7 @@ module SpecializedWorkers
|
|
|
196
202
|
{
|
|
197
203
|
rows_affected: affected,
|
|
198
204
|
data: nil,
|
|
199
|
-
time: rand(0.01..0.05)
|
|
205
|
+
time: rand(0.01..0.05),
|
|
200
206
|
}
|
|
201
207
|
end
|
|
202
208
|
|
|
@@ -207,7 +213,7 @@ module SpecializedWorkers
|
|
|
207
213
|
{
|
|
208
214
|
rows_affected: affected,
|
|
209
215
|
data: nil,
|
|
210
|
-
time: rand(0.01..0.03)
|
|
216
|
+
time: rand(0.01..0.03),
|
|
211
217
|
}
|
|
212
218
|
end
|
|
213
219
|
|
|
@@ -217,7 +223,7 @@ module SpecializedWorkers
|
|
|
217
223
|
{
|
|
218
224
|
rows_affected: 0,
|
|
219
225
|
data: nil,
|
|
220
|
-
time: rand(0.005..0.01)
|
|
226
|
+
time: rand(0.005..0.01),
|
|
221
227
|
}
|
|
222
228
|
end
|
|
223
229
|
end
|
|
@@ -230,14 +236,14 @@ module SpecializedWorkers
|
|
|
230
236
|
# Create separate supervisors for each worker type
|
|
231
237
|
@compute_supervisor = Fractor::Supervisor.new(
|
|
232
238
|
worker_pools: [
|
|
233
|
-
{ worker_class: ComputeWorker, num_workers: compute_workers }
|
|
234
|
-
]
|
|
239
|
+
{ worker_class: ComputeWorker, num_workers: compute_workers },
|
|
240
|
+
],
|
|
235
241
|
)
|
|
236
242
|
|
|
237
243
|
@db_supervisor = Fractor::Supervisor.new(
|
|
238
244
|
worker_pools: [
|
|
239
|
-
{ worker_class: DatabaseWorker, num_workers: db_workers }
|
|
240
|
-
]
|
|
245
|
+
{ worker_class: DatabaseWorker, num_workers: db_workers },
|
|
246
|
+
],
|
|
241
247
|
)
|
|
242
248
|
|
|
243
249
|
@compute_results = []
|
|
@@ -253,7 +259,8 @@ module SpecializedWorkers
|
|
|
253
259
|
|
|
254
260
|
# Create and add database work items
|
|
255
261
|
db_work_items = db_tasks.map do |task|
|
|
256
|
-
DatabaseWork.new(task[:data], task[:query_type], task[:table],
|
|
262
|
+
DatabaseWork.new(task[:data], task[:query_type], task[:table],
|
|
263
|
+
task[:conditions])
|
|
257
264
|
end
|
|
258
265
|
@db_supervisor.add_work_items(db_work_items)
|
|
259
266
|
|
|
@@ -277,13 +284,13 @@ module SpecializedWorkers
|
|
|
277
284
|
computation: {
|
|
278
285
|
tasks: compute_tasks.size,
|
|
279
286
|
completed: @compute_results.size,
|
|
280
|
-
results: @compute_results
|
|
287
|
+
results: @compute_results,
|
|
281
288
|
},
|
|
282
289
|
database: {
|
|
283
290
|
tasks: db_tasks.size,
|
|
284
291
|
completed: @db_results.size,
|
|
285
|
-
results: @db_results
|
|
286
|
-
}
|
|
292
|
+
results: @db_results,
|
|
293
|
+
},
|
|
287
294
|
}
|
|
288
295
|
end
|
|
289
296
|
|
|
@@ -316,18 +323,18 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
316
323
|
{
|
|
317
324
|
operation: :matrix_multiply,
|
|
318
325
|
data: "Matrix data...",
|
|
319
|
-
parameters: { size: [10, 10] }
|
|
326
|
+
parameters: { size: [10, 10] },
|
|
320
327
|
},
|
|
321
328
|
{
|
|
322
329
|
operation: :image_transform,
|
|
323
330
|
data: "Image data...",
|
|
324
|
-
parameters: { transforms: %i[rotate scale blur], angle: 45, scale: 1.5 }
|
|
331
|
+
parameters: { transforms: %i[rotate scale blur], angle: 45, scale: 1.5 },
|
|
325
332
|
},
|
|
326
333
|
{
|
|
327
334
|
operation: :path_finding,
|
|
328
335
|
data: "Graph data...",
|
|
329
|
-
parameters: { algorithm: :dijkstra, nodes: 20, start: 1, end: 15 }
|
|
330
|
-
}
|
|
336
|
+
parameters: { algorithm: :dijkstra, nodes: 20, start: 1, end: 15 },
|
|
337
|
+
},
|
|
331
338
|
]
|
|
332
339
|
|
|
333
340
|
# Prepare database tasks
|
|
@@ -335,24 +342,24 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
335
342
|
{
|
|
336
343
|
query_type: :select,
|
|
337
344
|
table: "users",
|
|
338
|
-
conditions: { active: true, role: "admin" }
|
|
345
|
+
conditions: { active: true, role: "admin" },
|
|
339
346
|
},
|
|
340
347
|
{
|
|
341
348
|
query_type: :insert,
|
|
342
349
|
table: "orders",
|
|
343
|
-
data: "Order data..."
|
|
350
|
+
data: "Order data...",
|
|
344
351
|
},
|
|
345
352
|
{
|
|
346
353
|
query_type: :update,
|
|
347
354
|
table: "products",
|
|
348
355
|
data: "Product data...",
|
|
349
|
-
conditions: { category: "electronics" }
|
|
356
|
+
conditions: { category: "electronics" },
|
|
350
357
|
},
|
|
351
358
|
{
|
|
352
359
|
query_type: :delete,
|
|
353
360
|
table: "sessions",
|
|
354
|
-
conditions: { expired: true }
|
|
355
|
-
}
|
|
361
|
+
conditions: { expired: true },
|
|
362
|
+
},
|
|
356
363
|
]
|
|
357
364
|
|
|
358
365
|
compute_workers = 2
|
|
@@ -363,7 +370,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
363
370
|
start_time = Time.now
|
|
364
371
|
system = SpecializedWorkers::HybridSystem.new(
|
|
365
372
|
compute_workers: compute_workers,
|
|
366
|
-
db_workers: db_workers
|
|
373
|
+
db_workers: db_workers,
|
|
367
374
|
)
|
|
368
375
|
result = system.process_mixed_workload(compute_tasks, db_tasks)
|
|
369
376
|
end_time = Time.now
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Fractor
|
|
6
|
+
# High-level wrapper for running Fractor in continuous mode.
|
|
7
|
+
# Handles threading, signal handling, and results processing automatically.
|
|
8
|
+
class ContinuousServer
|
|
9
|
+
attr_reader :supervisor, :work_queue
|
|
10
|
+
|
|
11
|
+
# Initialize a continuous server
|
|
12
|
+
# @param worker_pools [Array<Hash>] Worker pool configurations
|
|
13
|
+
# @param work_queue [WorkQueue, nil] Optional work queue to auto-register
|
|
14
|
+
# @param log_file [String, nil] Optional log file path
|
|
15
|
+
def initialize(worker_pools:, work_queue: nil, log_file: nil)
|
|
16
|
+
@worker_pools = worker_pools
|
|
17
|
+
@work_queue = work_queue
|
|
18
|
+
@log_file_path = log_file
|
|
19
|
+
@log_file = nil
|
|
20
|
+
@result_callbacks = []
|
|
21
|
+
@error_callbacks = []
|
|
22
|
+
@supervisor = nil
|
|
23
|
+
@supervisor_thread = nil
|
|
24
|
+
@results_thread = nil
|
|
25
|
+
@running = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Register a callback for successful results
|
|
29
|
+
# @yield [WorkResult] The successful result
|
|
30
|
+
def on_result(&block)
|
|
31
|
+
@result_callbacks << block
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Register a callback for errors
|
|
35
|
+
# @yield [WorkResult] The error result
|
|
36
|
+
def on_error(&block)
|
|
37
|
+
@error_callbacks << block
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Start the server and block until shutdown
|
|
41
|
+
# This method handles:
|
|
42
|
+
# - Opening log file if specified
|
|
43
|
+
# - Creating and starting supervisor
|
|
44
|
+
# - Starting results processing thread
|
|
45
|
+
# - Setting up signal handlers
|
|
46
|
+
# - Blocking until shutdown signal received
|
|
47
|
+
def run
|
|
48
|
+
setup_log_file
|
|
49
|
+
setup_supervisor
|
|
50
|
+
start_supervisor_thread
|
|
51
|
+
start_results_thread
|
|
52
|
+
|
|
53
|
+
log_message("Continuous server started")
|
|
54
|
+
log_message("Press Ctrl+C to stop")
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
# Block until shutdown
|
|
58
|
+
@supervisor_thread&.join
|
|
59
|
+
rescue Interrupt
|
|
60
|
+
log_message("Interrupt received, shutting down...")
|
|
61
|
+
ensure
|
|
62
|
+
cleanup
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Stop the server programmatically
|
|
67
|
+
def stop
|
|
68
|
+
return unless @running
|
|
69
|
+
|
|
70
|
+
log_message("Stopping continuous server...")
|
|
71
|
+
@running = false
|
|
72
|
+
|
|
73
|
+
@supervisor&.stop
|
|
74
|
+
|
|
75
|
+
# Wait for threads to finish
|
|
76
|
+
[@supervisor_thread, @results_thread].compact.each do |thread|
|
|
77
|
+
thread.join(2) if thread.alive?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
log_message("Continuous server stopped")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def setup_log_file
|
|
86
|
+
return unless @log_file_path
|
|
87
|
+
|
|
88
|
+
FileUtils.mkdir_p(File.dirname(@log_file_path))
|
|
89
|
+
@log_file = File.open(@log_file_path, "w")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def setup_supervisor
|
|
93
|
+
@supervisor = Supervisor.new(
|
|
94
|
+
worker_pools: @worker_pools,
|
|
95
|
+
continuous_mode: true,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Auto-register work queue if provided
|
|
99
|
+
if @work_queue
|
|
100
|
+
@work_queue.register_with_supervisor(@supervisor)
|
|
101
|
+
log_message(
|
|
102
|
+
"Work queue registered with supervisor (batch size: 10)",
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def start_supervisor_thread
|
|
108
|
+
@running = true
|
|
109
|
+
@supervisor_thread = Thread.new do
|
|
110
|
+
@supervisor.run
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
log_message("Supervisor error: #{e.message}")
|
|
113
|
+
log_message(e.backtrace.join("\n")) if ENV["FRACTOR_DEBUG"]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Give supervisor time to start up
|
|
117
|
+
sleep(0.1)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def start_results_thread
|
|
121
|
+
@results_thread = Thread.new do
|
|
122
|
+
log_message("Results processing thread started")
|
|
123
|
+
process_results_loop
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
log_message("Results thread error: #{e.message}")
|
|
126
|
+
log_message(e.backtrace.join("\n")) if ENV["FRACTOR_DEBUG"]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def process_results_loop
|
|
131
|
+
while @running
|
|
132
|
+
sleep(0.05)
|
|
133
|
+
|
|
134
|
+
process_successful_results
|
|
135
|
+
process_error_results
|
|
136
|
+
end
|
|
137
|
+
log_message("Results processing thread stopped")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def process_successful_results
|
|
141
|
+
loop do
|
|
142
|
+
result = @supervisor.results.results.shift
|
|
143
|
+
break unless result
|
|
144
|
+
|
|
145
|
+
@result_callbacks.each do |callback|
|
|
146
|
+
callback.call(result)
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
log_message("Error in result callback: #{e.message}")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def process_error_results
|
|
154
|
+
loop do
|
|
155
|
+
error_result = @supervisor.results.errors.shift
|
|
156
|
+
break unless error_result
|
|
157
|
+
|
|
158
|
+
@error_callbacks.each do |callback|
|
|
159
|
+
callback.call(error_result)
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
log_message("Error in error callback: #{e.message}")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def cleanup
|
|
167
|
+
@running = false
|
|
168
|
+
|
|
169
|
+
# Close log file if open
|
|
170
|
+
if @log_file && !@log_file.closed?
|
|
171
|
+
@log_file.close
|
|
172
|
+
@log_file = nil
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def log_message(message)
|
|
177
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")
|
|
178
|
+
log_entry = "[#{timestamp}] #{message}"
|
|
179
|
+
|
|
180
|
+
if @log_file && !@log_file.closed?
|
|
181
|
+
@log_file.puts(log_entry)
|
|
182
|
+
@log_file.flush
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
puts log_entry
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|