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 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
@@ -0,0 +1,9 @@
1
+ Sorry ...
2
+
3
+ - not ready for production yet
4
+ - not documented
5
+ - tests not published yet
6
+
7
+ License: MIT
8
+
9
+ Copyright (c) Cezary Baginski 2015
@@ -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
@@ -0,0 +1,3 @@
1
+ module Unravel
2
+ VERSION = "0.1.0"
3
+ 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: []