unravel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []