unravel 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/README.md +9 -0
- data/lib/unravel/exec.rb +37 -0
- data/lib/unravel/version.rb +3 -0
- data/lib/unravel.rb +228 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fe134526bae88b0b4e53966eec2efd0cc8832e03
|
4
|
+
data.tar.gz: 42229f569f49a16a686451ae489db749e475b749
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 86ed68678c6af3fa02c67ab1fdc4ee5b54b56ca1efecaf240245209def1293e95e821d383f472b009ce67139bd5e4916607d11652a28b97a41f400a0c5780cd5
|
7
|
+
data.tar.gz: 1dfb97a099192ade124fd319f1213e21db3a66d841f70e50a41cef2e7d6e856e7ed630513a492f239f5b2ab320fe4983ba30887db8c8572daa76f0a8533e206e
|
data/README.md
ADDED
data/lib/unravel/exec.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Unravel
|
2
|
+
class Exec
|
3
|
+
class Error < Unravel::HumanInterventionNeeded; end
|
4
|
+
end
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def run(*args)
|
8
|
+
Exec(args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def Exec(args)
|
12
|
+
Unravel.logger.debug " -> Running: #{args.inspect}"
|
13
|
+
out, error, status = Open3.capture3(*args)
|
14
|
+
Unravel.logger.debug "Output from #{args.inspect}: -----"
|
15
|
+
Unravel.logger.debug "#{out}"
|
16
|
+
return true if status.success?
|
17
|
+
Unravel.logger.debug "Errors from #{args.inspect}: -----"
|
18
|
+
Unravel.logger.debug "#{error}"
|
19
|
+
error = out if error.strip.empty?
|
20
|
+
raise Exec::Error, error
|
21
|
+
rescue Errno::ENOENT => e
|
22
|
+
raise Exec::Error, e.message
|
23
|
+
end
|
24
|
+
|
25
|
+
def Capture(args)
|
26
|
+
Unravel.logger.debug " -> Running: #{args.inspect}"
|
27
|
+
out, error, status = Open3.capture3(*args)
|
28
|
+
return out if status.success?
|
29
|
+
Unravel.logger.debug "Errors from #{args.inspect}: -----"
|
30
|
+
Unravel.logger.debug "#{error}"
|
31
|
+
error = out if error.strip.empty?
|
32
|
+
raise Exec::Error, error
|
33
|
+
rescue Errno::ENOENT => e
|
34
|
+
raise Exec::Error, e.message
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/unravel.rb
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'open3'
|
5
|
+
require 'set'
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
# TODO: don't allow replacing the error
|
9
|
+
|
10
|
+
module Unravel
|
11
|
+
def self.logger
|
12
|
+
@@logger ||= Logger.new(STDOUT).tap do |logger|
|
13
|
+
logger.level = Logger::DEBUG
|
14
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
15
|
+
"#{severity}: #{msg}\n"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class HumanInterventionNeeded < RuntimeError; end
|
21
|
+
class NoKnownRootCause < HumanInterventionNeeded ; end
|
22
|
+
class SameCauseReoccurringCause < HumanInterventionNeeded; end
|
23
|
+
|
24
|
+
class Registry
|
25
|
+
attr_reader :achievements, :symptoms, :fixes, :contexts, :errors
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@fixes = {}
|
29
|
+
@symptoms = {}
|
30
|
+
@achievements = {}
|
31
|
+
@contexts = {}
|
32
|
+
@errors = {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_fix(name)
|
36
|
+
@fixes[name].tap do |cause|
|
37
|
+
fail HumanInterventionNeeded, "No fix for: #{name}" unless cause
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_fix(name, block)
|
42
|
+
fail HumanInterventionNeeded, "fix already exists: #{name}" if @fixes.key?(name)
|
43
|
+
@fixes[name] = block
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_root_cause(symptom)
|
47
|
+
@symptoms[symptom]
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_symptom(symptom, root_cause)
|
51
|
+
@symptoms[symptom] = root_cause
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_achievement(name, &block)
|
55
|
+
@achievements[name] = block
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_achievement(name)
|
59
|
+
@achievements[name].tap do |achievement|
|
60
|
+
fail HumanInterventionNeeded, "No such achievement: #{name.inspect}" unless achievement
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_error_contexts(name, contexts)
|
65
|
+
@contexts[name] ||= contexts
|
66
|
+
end
|
67
|
+
|
68
|
+
def error_contexts_for_achievement(name)
|
69
|
+
@contexts[name].tap do |context|
|
70
|
+
fail HumanInterventionNeeded, "No error handlers for achievement: #{name}" unless context
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def fixable_error(name)
|
75
|
+
@errors[name]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Session
|
80
|
+
attr_reader :registry
|
81
|
+
|
82
|
+
class FixableError < RuntimeError
|
83
|
+
attr_reader :symptom
|
84
|
+
def initialize(symptom_name)
|
85
|
+
@symptom = symptom_name
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize
|
90
|
+
@registry = Registry.new
|
91
|
+
end
|
92
|
+
|
93
|
+
def achieve(name, max_retries = 5)
|
94
|
+
check # TODO: overhead?
|
95
|
+
|
96
|
+
prev_causes = Set.new
|
97
|
+
retries_left = max_retries
|
98
|
+
|
99
|
+
begin
|
100
|
+
Unravel.logger.info("Achieving (attempt: #{max_retries - retries_left + 1}/#{max_retries}): #{name.inspect}")
|
101
|
+
|
102
|
+
block = registry.get_achievement(name)
|
103
|
+
error_contexts = registry.error_contexts_for_achievement(name)
|
104
|
+
|
105
|
+
begin
|
106
|
+
res = return_wrap(&block)
|
107
|
+
rescue *error_contexts.keys
|
108
|
+
ex = $!
|
109
|
+
econtext = error_contexts[ex.class]
|
110
|
+
unless econtext
|
111
|
+
# TODO: not tested
|
112
|
+
fail "No error context given for #{name} to handle exception: #{ex.message}"
|
113
|
+
end
|
114
|
+
|
115
|
+
econtext.each do |fix_name|
|
116
|
+
error = $!.message
|
117
|
+
fix! fix_name, error
|
118
|
+
end
|
119
|
+
fail
|
120
|
+
end
|
121
|
+
|
122
|
+
return true if res == true
|
123
|
+
fail NotImplementedError, "#{name} unexpectedly returned #{res.inspect} (expected true or exception)"
|
124
|
+
|
125
|
+
rescue FixableError => error
|
126
|
+
Unravel.logger.info("#{name}: Symptom: #{error.symptom.inspect}")
|
127
|
+
|
128
|
+
#Unravel.logger.debug(" -> failed: #{name.inspect}: #{error.symptom.inspect}\n")
|
129
|
+
cause = get_root_cause_for(error.symptom)
|
130
|
+
|
131
|
+
fail NoKnownRootCause, "Can't find root cause for: #{error.symptom}, #{error.message}" unless cause
|
132
|
+
|
133
|
+
Unravel.logger.info("#{name}: Cause: #{cause.inspect}")
|
134
|
+
if prev_causes.include? cause
|
135
|
+
fail SameCauseReoccurringCause, "#{cause.to_s} wasn't ultimately fixed (it occured again)"
|
136
|
+
end
|
137
|
+
|
138
|
+
prev_causes << cause
|
139
|
+
recipe_for(cause).call
|
140
|
+
|
141
|
+
retries_left -= 1
|
142
|
+
retry if retries_left > 0
|
143
|
+
fail
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def achievement(name, error_contexts, &block)
|
148
|
+
registry.add_achievement(name, &block)
|
149
|
+
registry.add_error_contexts(name, error_contexts)
|
150
|
+
end
|
151
|
+
|
152
|
+
def error
|
153
|
+
registry.errors
|
154
|
+
end
|
155
|
+
|
156
|
+
def root_cause_for(mapping)
|
157
|
+
symptom, root_cause = *mapping.first
|
158
|
+
registry.add_symptom(symptom, root_cause)
|
159
|
+
end
|
160
|
+
|
161
|
+
#TODO: move logic to registry
|
162
|
+
def fix_for(*args, &block)
|
163
|
+
name, achievement = *args
|
164
|
+
if block_given?
|
165
|
+
if args.size > 1
|
166
|
+
fail ArgumentError, "#{args[1..-1].inspect} ignored because of block"
|
167
|
+
end
|
168
|
+
registry.add_fix(name, block)
|
169
|
+
else
|
170
|
+
if name.is_a?(Hash)
|
171
|
+
name, achievement = *name.first
|
172
|
+
end
|
173
|
+
fix_for(name) { achieve achievement } unless block_given?
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Shorthand for easy-to-fix and name problems
|
178
|
+
def quickfix(error_name, regexp, fix_name, handlers={})
|
179
|
+
root_cause_name = "no_#{fix_name}".to_sym
|
180
|
+
error[error_name] = regexp
|
181
|
+
root_cause_for error_name => root_cause_name
|
182
|
+
fix_for root_cause_name => fix_name
|
183
|
+
achievement fix_name, handlers, &method(fix_name)
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def return_wrap(&block)
|
189
|
+
Thread.new { return block.yield }.join
|
190
|
+
rescue LocalJumpError => ex
|
191
|
+
ex.exit_value
|
192
|
+
end
|
193
|
+
|
194
|
+
def check
|
195
|
+
logger = Unravel.logger
|
196
|
+
|
197
|
+
res = registry.fixes.keys - registry.symptoms.values
|
198
|
+
logger.warn "Unused: #{res.inspect}" unless res.empty?
|
199
|
+
|
200
|
+
res = registry.symptoms.values - registry.fixes.keys
|
201
|
+
logger.warn "Unhandled: #{res.inspect}" unless res.empty?
|
202
|
+
|
203
|
+
errors = registry.contexts.values.map(&:values).flatten(2)
|
204
|
+
res = errors - registry.symptoms.keys
|
205
|
+
logger.warn "Unknown contexts: #{res.inspect}" unless res.empty?
|
206
|
+
|
207
|
+
res = registry.symptoms.keys - errors
|
208
|
+
logger.warn "Unused errors: #{res.inspect}" unless res.empty?
|
209
|
+
end
|
210
|
+
|
211
|
+
def fix!(name, error)
|
212
|
+
regexp = registry.fixable_error(name)
|
213
|
+
unless regexp
|
214
|
+
fail HumanInterventionNeeded, "Unregistered error: #{name} to match #{error.inspect}"
|
215
|
+
end
|
216
|
+
# TODO: encoding not tested
|
217
|
+
fail FixableError.new(name) if regexp.match(error.force_encoding(Encoding::ASCII_8BIT))
|
218
|
+
end
|
219
|
+
|
220
|
+
def get_root_cause_for(symptom)
|
221
|
+
registry.get_root_cause(symptom)
|
222
|
+
end
|
223
|
+
|
224
|
+
def recipe_for(name, &block)
|
225
|
+
registry.get_fix(name)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: unravel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cezary Baginski <cezary@chronomantic.net>
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-07-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Tool for solving non-deterministic problems given goals, symptoms, fixes
|
14
|
+
and root causes
|
15
|
+
email: cezary@chronomantic.net
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- lib/unravel.rb
|
22
|
+
- lib/unravel/exec.rb
|
23
|
+
- lib/unravel/version.rb
|
24
|
+
homepage: https://github.com/e2/unravel
|
25
|
+
licenses:
|
26
|
+
- mit
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 2.4.5
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: Solves complex non-deterministic problems given symptoms, causes and fixes
|
48
|
+
test_files: []
|