bunny_farm 0.1.2
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/.envrc +1 -0
- data/.github/workflows/docs.yml +38 -0
- data/.gitignore +11 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +61 -0
- data/COMMITS.md +196 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +330 -0
- data/Rakefile +9 -0
- data/bunny_farm.gemspec +30 -0
- data/config/bunny.yml.erb +29 -0
- data/config/bunny_test.yml.erb +29 -0
- data/config/hipchat.yml.erb +12 -0
- data/docs/api/configuration.md +9 -0
- data/docs/api/consumer.md +8 -0
- data/docs/api/message-class.md +419 -0
- data/docs/api/publisher.md +9 -0
- data/docs/architecture/integration.md +8 -0
- data/docs/architecture/message-flow.md +11 -0
- data/docs/architecture/overview.md +448 -0
- data/docs/architecture/scaling.md +8 -0
- data/docs/assets/actions_dsl_flow.svg +109 -0
- data/docs/assets/architecture_overview.svg +152 -0
- data/docs/assets/best_practices_patterns.svg +203 -0
- data/docs/assets/bunny_farm_logo.png +0 -0
- data/docs/assets/configuration_api_methods.svg +104 -0
- data/docs/assets/configuration_flow.svg +130 -0
- data/docs/assets/configuration_hierarchy.svg +70 -0
- data/docs/assets/data_processing_pipeline.svg +131 -0
- data/docs/assets/debugging_monitoring.svg +165 -0
- data/docs/assets/ecommerce_example_flow.svg +145 -0
- data/docs/assets/email_campaign_example.svg +127 -0
- data/docs/assets/environment_variables_map.svg +78 -0
- data/docs/assets/error_handling_flow.svg +114 -0
- data/docs/assets/favicon.ico +1 -0
- data/docs/assets/fields_dsl_structure.svg +89 -0
- data/docs/assets/instance_methods_lifecycle.svg +137 -0
- data/docs/assets/integration_patterns.svg +207 -0
- data/docs/assets/json_serialization_flow.svg +153 -0
- data/docs/assets/logo.svg +4 -0
- data/docs/assets/message_api_overview.svg +126 -0
- data/docs/assets/message_encapsulation.svg +113 -0
- data/docs/assets/message_lifecycle.svg +110 -0
- data/docs/assets/message_structure.svg +138 -0
- data/docs/assets/publisher_consumer_api.svg +120 -0
- data/docs/assets/scaling_deployment_patterns.svg +195 -0
- data/docs/assets/smart_routing_diagram.svg +131 -0
- data/docs/assets/system_architecture_overview.svg +155 -0
- data/docs/assets/task_scheduling_flow.svg +139 -0
- data/docs/assets/testing_strategies.svg +146 -0
- data/docs/assets/workflow_patterns.svg +183 -0
- data/docs/assets/yaml_config_structure.svg +72 -0
- data/docs/configuration/environment-variables.md +14 -0
- data/docs/configuration/overview.md +373 -0
- data/docs/configuration/programmatic-setup.md +10 -0
- data/docs/configuration/yaml-configuration.md +12 -0
- data/docs/core-features/configuration.md +528 -0
- data/docs/core-features/error-handling.md +82 -0
- data/docs/core-features/json-serialization.md +545 -0
- data/docs/core-features/message-design.md +406 -0
- data/docs/core-features/smart-routing.md +467 -0
- data/docs/core-features/task-scheduling.md +67 -0
- data/docs/core-features/workflow-support.md +112 -0
- data/docs/development/contributing.md +345 -0
- data/docs/development/roadmap.md +9 -0
- data/docs/development/testing.md +14 -0
- data/docs/examples/order-processing.md +10 -0
- data/docs/examples/overview.md +269 -0
- data/docs/examples/real-world.md +8 -0
- data/docs/examples/simple-producer-consumer.md +15 -0
- data/docs/examples/task-scheduler.md +9 -0
- data/docs/getting-started/basic-concepts.md +274 -0
- data/docs/getting-started/installation.md +122 -0
- data/docs/getting-started/quick-start.md +158 -0
- data/docs/index.md +106 -0
- data/docs/message-structure/actions-dsl.md +163 -0
- data/docs/message-structure/fields-dsl.md +146 -0
- data/docs/message-structure/instance-methods.md +115 -0
- data/docs/message-structure/overview.md +211 -0
- data/examples/README.md +212 -0
- data/examples/consumer.rb +41 -0
- data/examples/images/message_flow.svg +87 -0
- data/examples/images/order_workflow.svg +122 -0
- data/examples/images/producer_consumer.svg +96 -0
- data/examples/images/task_scheduler.svg +140 -0
- data/examples/order_processor.rb +238 -0
- data/examples/producer.rb +60 -0
- data/examples/simple_message.rb +43 -0
- data/examples/task_scheduler.rb +263 -0
- data/images/architecture_overview.svg +152 -0
- data/images/bunny_farm_logo.png +0 -0
- data/images/configuration_flow.svg +130 -0
- data/images/message_structure.svg +138 -0
- data/lib/bunny_farm/.irbrc +7 -0
- data/lib/bunny_farm/generic_consumer.rb +12 -0
- data/lib/bunny_farm/hash_ext.rb +37 -0
- data/lib/bunny_farm/init_bunny.rb +137 -0
- data/lib/bunny_farm/init_hipchat.rb +49 -0
- data/lib/bunny_farm/message.rb +218 -0
- data/lib/bunny_farm/message_elements.rb +25 -0
- data/lib/bunny_farm/version.rb +3 -0
- data/lib/bunny_farm.rb +9 -0
- data/mkdocs.yml +148 -0
- metadata +244 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<svg width="800" height="650" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Background transparent -->
|
|
3
|
+
<rect width="800" height="650" fill="transparent"/>
|
|
4
|
+
|
|
5
|
+
<!-- Title -->
|
|
6
|
+
<text x="400" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="20" font-weight="bold" fill="#e0e0e0">
|
|
7
|
+
BunnyFarm Message Class Structure
|
|
8
|
+
</text>
|
|
9
|
+
|
|
10
|
+
<!-- Class Definition -->
|
|
11
|
+
<g>
|
|
12
|
+
<rect x="50" y="60" width="700" height="120" rx="10" fill="#2d3748" stroke="#9f7aea" stroke-width="2"/>
|
|
13
|
+
<text x="400" y="85" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#9f7aea">Message Class Definition</text>
|
|
14
|
+
|
|
15
|
+
<rect x="70" y="100" width="660" height="70" fill="#1a202c" stroke="#4a5568" stroke-width="1"/>
|
|
16
|
+
<text x="80" y="115" font-family="monospace" font-size="11" fill="#9f7aea">class OrderMessage < BunnyFarm::Message</text>
|
|
17
|
+
<text x="90" y="130" font-family="monospace" font-size="11" fill="#68d391">fields</text>
|
|
18
|
+
<text x="130" y="130" font-family="monospace" font-size="11" fill="#e0e0e0">:order_id, :total, { customer: [:name, :email] }, { items: [:id, :qty] }</text>
|
|
19
|
+
<text x="90" y="145" font-family="monospace" font-size="11" fill="#f6e05e">actions</text>
|
|
20
|
+
<text x="130" y="145" font-family="monospace" font-size="11" fill="#e0e0e0">:validate, :process_payment, :ship, :cancel</text>
|
|
21
|
+
<text x="80" y="160" font-family="monospace" font-size="11" fill="#9f7aea">end</text>
|
|
22
|
+
</g>
|
|
23
|
+
|
|
24
|
+
<!-- DSL Components -->
|
|
25
|
+
<g>
|
|
26
|
+
<rect x="50" y="200" width="340" height="160" rx="10" fill="#2d3748" stroke="#68d391" stroke-width="2"/>
|
|
27
|
+
<text x="220" y="225" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#68d391">Fields DSL</text>
|
|
28
|
+
<text x="220" y="245" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#e0e0e0">Define expected data structure</text>
|
|
29
|
+
|
|
30
|
+
<rect x="70" y="255" width="300" height="95" fill="#1a202c" stroke="#4a5568" stroke-width="1"/>
|
|
31
|
+
<text x="80" y="270" font-family="monospace" font-size="10" fill="#68d391">Simple fields:</text>
|
|
32
|
+
<text x="80" y="285" font-family="monospace" font-size="10" fill="#e0e0e0">:name, :email, :amount</text>
|
|
33
|
+
|
|
34
|
+
<text x="80" y="305" font-family="monospace" font-size="10" fill="#68d391">Nested objects:</text>
|
|
35
|
+
<text x="80" y="320" font-family="monospace" font-size="10" fill="#e0e0e0">{ customer: [:name, :email] }</text>
|
|
36
|
+
|
|
37
|
+
<text x="80" y="340" font-family="monospace" font-size="10" fill="#68d391">Arrays of objects:</text>
|
|
38
|
+
<text x="80" y="355" font-family="monospace" font-size="10" fill="#e0e0e0">{ items: [:product_id, :quantity] }</text>
|
|
39
|
+
|
|
40
|
+
<rect x="410" y="200" width="340" height="160" rx="10" fill="#2d3748" stroke="#f6e05e" stroke-width="2"/>
|
|
41
|
+
<text x="580" y="225" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#f6e05e">Actions DSL</text>
|
|
42
|
+
<text x="580" y="245" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#e0e0e0">Define available operations</text>
|
|
43
|
+
|
|
44
|
+
<rect x="430" y="255" width="300" height="95" fill="#1a202c" stroke="#4a5568" stroke-width="1"/>
|
|
45
|
+
<text x="440" y="270" font-family="monospace" font-size="10" fill="#f6e05e">actions :create, :update, :delete</text>
|
|
46
|
+
<text x="440" y="290" font-family="monospace" font-size="10" fill="#9ca3af"># Each action becomes a method</text>
|
|
47
|
+
<text x="440" y="310" font-family="monospace" font-size="10" fill="#f6e05e">def create(*args)</text>
|
|
48
|
+
<text x="450" y="325" font-family="monospace" font-size="10" fill="#e0e0e0"># Process creation logic</text>
|
|
49
|
+
<text x="450" y="340" font-family="monospace" font-size="10" fill="#68d391">success!</text>
|
|
50
|
+
<text x="440" y="355" font-family="monospace" font-size="10" fill="#f6e05e">end</text>
|
|
51
|
+
</g>
|
|
52
|
+
|
|
53
|
+
<!-- Message Instance Structure -->
|
|
54
|
+
<g>
|
|
55
|
+
<rect x="50" y="380" width="700" height="140" rx="10" fill="#2d3748" stroke="#4299e1" stroke-width="2"/>
|
|
56
|
+
<text x="400" y="405" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#4299e1">Message Instance Structure</text>
|
|
57
|
+
|
|
58
|
+
<!-- Instance Variables -->
|
|
59
|
+
<rect x="80" y="420" width="200" height="90" rx="5" fill="#1a202c" stroke="#4a5568" stroke-width="1"/>
|
|
60
|
+
<text x="180" y="440" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#4299e1">Instance Variables</text>
|
|
61
|
+
<text x="90" y="455" font-family="monospace" font-size="10" fill="#68d391">@items</text>
|
|
62
|
+
<text x="125" y="455" font-family="monospace" font-size="10" fill="#e0e0e0">- Validated data</text>
|
|
63
|
+
<text x="90" y="470" font-family="monospace" font-size="10" fill="#68d391">@elements</text>
|
|
64
|
+
<text x="140" y="470" font-family="monospace" font-size="10" fill="#e0e0e0">- Raw JSON data</text>
|
|
65
|
+
<text x="90" y="485" font-family="monospace" font-size="10" fill="#68d391">@payload</text>
|
|
66
|
+
<text x="135" y="485" font-family="monospace" font-size="10" fill="#e0e0e0">- JSON string</text>
|
|
67
|
+
<text x="90" y="500" font-family="monospace" font-size="10" fill="#68d391">@errors</text>
|
|
68
|
+
<text x="130" y="500" font-family="monospace" font-size="10" fill="#e0e0e0">- Error messages</text>
|
|
69
|
+
|
|
70
|
+
<!-- Access Methods -->
|
|
71
|
+
<rect x="300" y="420" width="180" height="90" rx="5" fill="#1a202c" stroke="#4a5568" stroke-width="1"/>
|
|
72
|
+
<text x="390" y="440" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#4299e1">Access Methods</text>
|
|
73
|
+
<text x="310" y="455" font-family="monospace" font-size="10" fill="#68d391">msg[:field]</text>
|
|
74
|
+
<text x="370" y="455" font-family="monospace" font-size="10" fill="#e0e0e0">- Get value</text>
|
|
75
|
+
<text x="310" y="470" font-family="monospace" font-size="10" fill="#68d391">msg[:field] = val</text>
|
|
76
|
+
<text x="395" y="470" font-family="monospace" font-size="10" fill="#e0e0e0">- Set</text>
|
|
77
|
+
<text x="310" y="485" font-family="monospace" font-size="10" fill="#68d391">msg.to_json</text>
|
|
78
|
+
<text x="370" y="485" font-family="monospace" font-size="10" fill="#e0e0e0">- Serialize</text>
|
|
79
|
+
<text x="310" y="500" font-family="monospace" font-size="10" fill="#68d391">msg.keys</text>
|
|
80
|
+
<text x="350" y="500" font-family="monospace" font-size="10" fill="#e0e0e0">- Get fields</text>
|
|
81
|
+
|
|
82
|
+
<!-- State Methods -->
|
|
83
|
+
<rect x="500" y="420" width="220" height="90" rx="5" fill="#1a202c" stroke="#4a5568" stroke-width="1"/>
|
|
84
|
+
<text x="610" y="440" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#4299e1">State Management</text>
|
|
85
|
+
<text x="510" y="455" font-family="monospace" font-size="10" fill="#68d391">success!</text>
|
|
86
|
+
<text x="550" y="455" font-family="monospace" font-size="10" fill="#e0e0e0">- Mark successful</text>
|
|
87
|
+
<text x="510" y="470" font-family="monospace" font-size="10" fill="#e53e3e">failure(msg)</text>
|
|
88
|
+
<text x="570" y="470" font-family="monospace" font-size="10" fill="#e0e0e0">- Mark failed</text>
|
|
89
|
+
<text x="510" y="485" font-family="monospace" font-size="10" fill="#68d391">successful?</text>
|
|
90
|
+
<text x="570" y="485" font-family="monospace" font-size="10" fill="#e0e0e0">- Check status</text>
|
|
91
|
+
<text x="510" y="500" font-family="monospace" font-size="10" fill="#68d391">publish(action)</text>
|
|
92
|
+
<text x="585" y="500" font-family="monospace" font-size="10" fill="#e0e0e0">- Send msg</text>
|
|
93
|
+
</g>
|
|
94
|
+
|
|
95
|
+
<!-- Data Flow Example -->
|
|
96
|
+
<g>
|
|
97
|
+
<rect x="50" y="540" width="700" height="100" rx="10" fill="#2d3748" stroke="#ed8936" stroke-width="2"/>
|
|
98
|
+
<text x="400" y="565" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#ed8936">Data Flow Example</text>
|
|
99
|
+
|
|
100
|
+
<!-- Step boxes -->
|
|
101
|
+
<rect x="70" y="580" width="120" height="50" rx="5" fill="#68d391"/>
|
|
102
|
+
<text x="130" y="595" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" font-weight="bold" fill="#1a202c">1. JSON Input</text>
|
|
103
|
+
<text x="130" y="610" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#1a202c">Raw message data</text>
|
|
104
|
+
<text x="130" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#1a202c">from RabbitMQ</text>
|
|
105
|
+
|
|
106
|
+
<rect x="210" y="580" width="120" height="50" rx="5" fill="#4299e1"/>
|
|
107
|
+
<text x="270" y="595" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" font-weight="bold" fill="#1a202c">2. Parse & Validate</text>
|
|
108
|
+
<text x="270" y="610" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#1a202c">Extract fields</text>
|
|
109
|
+
<text x="270" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#1a202c">using DSL definition</text>
|
|
110
|
+
|
|
111
|
+
<rect x="350" y="580" width="120" height="50" rx="5" fill="#9f7aea"/>
|
|
112
|
+
<text x="410" y="595" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" font-weight="bold" fill="#1a202c">3. Route Action</text>
|
|
113
|
+
<text x="410" y="610" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#1a202c">Call method based</text>
|
|
114
|
+
<text x="410" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#1a202c">on routing key</text>
|
|
115
|
+
|
|
116
|
+
<rect x="490" y="580" width="120" height="50" rx="5" fill="#f6e05e"/>
|
|
117
|
+
<text x="550" y="595" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" font-weight="bold" fill="#1a202c">4. Execute Logic</text>
|
|
118
|
+
<text x="550" y="610" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#1a202c">Run business</text>
|
|
119
|
+
<text x="550" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#1a202c">logic in action</text>
|
|
120
|
+
|
|
121
|
+
<rect x="630" y="580" width="120" height="50" rx="5" fill="#e53e3e"/>
|
|
122
|
+
<text x="690" y="595" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" font-weight="bold" fill="#1a202c">5. ACK/NACK</text>
|
|
123
|
+
<text x="690" y="610" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#1a202c">Acknowledge</text>
|
|
124
|
+
<text x="690" y="620" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#1a202c">to RabbitMQ</text>
|
|
125
|
+
|
|
126
|
+
<!-- Arrows -->
|
|
127
|
+
<defs>
|
|
128
|
+
<marker id="arrow-flow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
129
|
+
<polygon points="0 0, 10 3.5, 0 7" fill="#ed8936"/>
|
|
130
|
+
</marker>
|
|
131
|
+
</defs>
|
|
132
|
+
|
|
133
|
+
<line x1="190" y1="605" x2="210" y2="605" stroke="#ed8936" stroke-width="2" marker-end="url(#arrow-flow)"/>
|
|
134
|
+
<line x1="330" y1="605" x2="350" y2="605" stroke="#ed8936" stroke-width="2" marker-end="url(#arrow-flow)"/>
|
|
135
|
+
<line x1="470" y1="605" x2="490" y2="605" stroke="#ed8936" stroke-width="2" marker-end="url(#arrow-flow)"/>
|
|
136
|
+
<line x1="610" y1="605" x2="630" y2="605" stroke="#ed8936" stroke-width="2" marker-end="url(#arrow-flow)"/>
|
|
137
|
+
</g>
|
|
138
|
+
</svg>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'active_support/core_ext/hash/keys'
|
|
2
|
+
|
|
3
|
+
class Hash
|
|
4
|
+
|
|
5
|
+
# Extract a limited set of keys from a hash
|
|
6
|
+
# SMELL: this will mess up on hash entires that are arrays of hashes.
|
|
7
|
+
def tdv_extract(an_array)
|
|
8
|
+
temp_hash = {}
|
|
9
|
+
an_array.each do |s|
|
|
10
|
+
if s.is_a? Symbol
|
|
11
|
+
temp_hash[s] = self[s]
|
|
12
|
+
elsif s.is_a? Hash
|
|
13
|
+
# FIXME: Why don't we allow all the keys?
|
|
14
|
+
the_entry_name = s.keys.first
|
|
15
|
+
the_component_names = s[the_entry_name]
|
|
16
|
+
if self[the_entry_name].is_a? Hash
|
|
17
|
+
temp_hash[the_entry_name] = self[the_entry_name].tdv_extract(the_component_names)
|
|
18
|
+
elsif self[the_entry_name].is_a? Array
|
|
19
|
+
temp_hash[the_entry_name] = []
|
|
20
|
+
self[the_entry_name].each do |a|
|
|
21
|
+
temp_hash[the_entry_name] << a.tdv_extract(the_component_names)
|
|
22
|
+
end
|
|
23
|
+
elsif self[the_entry_name].is_a? NilClass
|
|
24
|
+
next
|
|
25
|
+
else
|
|
26
|
+
raise "Another Invalid entry #{the_entry_name} of #{self[the_entry_name].class}"
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
raise "Invalid entry name #{s} of class #{s.class}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
return temp_hash
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end # class Hash
|
|
36
|
+
|
|
37
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'bunny'
|
|
2
|
+
require 'erb'
|
|
3
|
+
require 'hashie'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
require_relative 'generic_consumer'
|
|
7
|
+
|
|
8
|
+
module BunnyFarm
|
|
9
|
+
|
|
10
|
+
CONFIG = Hashie::Mash.new
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
|
|
14
|
+
def config(config_file=nil, &block)
|
|
15
|
+
|
|
16
|
+
unless config_file.nil?
|
|
17
|
+
config_dir File.dirname(config_file)
|
|
18
|
+
bunny_file File.basename(config_file)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if block_given?
|
|
22
|
+
class_eval(&block)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
default :env, 'development'
|
|
26
|
+
|
|
27
|
+
default :config_dir, File.dirname(__FILE__) + "/../../config/"
|
|
28
|
+
|
|
29
|
+
default :bunny_file, 'bunny.yml.erb'
|
|
30
|
+
|
|
31
|
+
default :bunny, Hashie::Mash.new(
|
|
32
|
+
YAML.load(
|
|
33
|
+
ERB.new(
|
|
34
|
+
File.read(
|
|
35
|
+
File.join( CONFIG.config_dir,
|
|
36
|
+
CONFIG.bunny_file))).result, aliases: true))[CONFIG.env]
|
|
37
|
+
|
|
38
|
+
default :app_id, CONFIG.bunny.app_name
|
|
39
|
+
|
|
40
|
+
default :connection, Bunny.new(CONFIG.bunny).tap(&:start)
|
|
41
|
+
|
|
42
|
+
default :channel, CONFIG.connection.create_channel
|
|
43
|
+
|
|
44
|
+
default :exchange, CONFIG.channel.topic(
|
|
45
|
+
CONFIG.bunny.exchange_name,
|
|
46
|
+
:durable => true,
|
|
47
|
+
:auto_delete => false)
|
|
48
|
+
|
|
49
|
+
default :queue, CONFIG.channel.queue(
|
|
50
|
+
CONFIG.bunny.queue_name,
|
|
51
|
+
durable: true,
|
|
52
|
+
auto_delete: false,
|
|
53
|
+
arguments: {"x-max-length" => 1000}
|
|
54
|
+
).bind(CONFIG.exchange,
|
|
55
|
+
:routing_key => CONFIG.bunny.routing_key)
|
|
56
|
+
|
|
57
|
+
default :control_c, false # used to signal that user hit the panic button
|
|
58
|
+
|
|
59
|
+
default :consumer_tag, 'generic_consumer'
|
|
60
|
+
|
|
61
|
+
default :no_ack, false # false means that an acknowledgement is required
|
|
62
|
+
default :exclusive, false
|
|
63
|
+
|
|
64
|
+
# Usage: see BunnyFarm#manage
|
|
65
|
+
default :run, GenericConsumer.new(
|
|
66
|
+
CONFIG.channel,
|
|
67
|
+
CONFIG.queue,
|
|
68
|
+
CONFIG.consumer_tag,
|
|
69
|
+
CONFIG.no_ack,
|
|
70
|
+
CONFIG.exclusive
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
default :block, true # set to false for background processing
|
|
74
|
+
|
|
75
|
+
end # def self.config
|
|
76
|
+
|
|
77
|
+
def default(key, value)
|
|
78
|
+
set(key, value) if CONFIG[key].nil?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def set(key, value)
|
|
82
|
+
STDERR.puts "Setting #{key} ..." if defined?($debug) && $debug
|
|
83
|
+
CONFIG[key] = value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def method_missing(method_sym, *args, &block)
|
|
87
|
+
STDERR.puts "METHOD-MISSING: #{method_sym}(#{args.join(', ')})" if defined?($debug) && $debug
|
|
88
|
+
set(method_sym, args.first)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Usage:
|
|
92
|
+
# BunnyFarm.manage(true) -- manages queue in background
|
|
93
|
+
# BunnyFarm.manage(false) -- manages queue in foreground (eg. blocks; will not return)
|
|
94
|
+
# BunnyFarm.manage -- behavior controlled by BunnyFarm::CONFIG.block
|
|
95
|
+
def manage(background=nil)
|
|
96
|
+
|
|
97
|
+
if background.nil?
|
|
98
|
+
background = CONFIG.block
|
|
99
|
+
else
|
|
100
|
+
background = !background # Counter-intutive based on POV of block
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
CONFIG.run.on_delivery do |info, metadata, payload|
|
|
104
|
+
Thread.new do
|
|
105
|
+
klass = Kernel.const_get info.routing_key.split('.').first
|
|
106
|
+
klass.new(payload, info, metadata)
|
|
107
|
+
end
|
|
108
|
+
end # BunnyFarm.run.on_delivery do
|
|
109
|
+
|
|
110
|
+
CONFIG.queue.subscribe_with( CONFIG.run, block: background )
|
|
111
|
+
|
|
112
|
+
end # def self.manage
|
|
113
|
+
end # class << self
|
|
114
|
+
end # module BunnyFarm
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
trap("INT") do
|
|
118
|
+
BunnyFarm::CONFIG.control_c = true
|
|
119
|
+
exit
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Need to clean up the smart pills -- rabbit joke
|
|
123
|
+
at_exit do
|
|
124
|
+
|
|
125
|
+
if BunnyFarm::CONFIG.control_c
|
|
126
|
+
STDERR.puts 'Shutting down the BunnyFarm due to control-C request'
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
unless BunnyFarm::CONFIG.channel.nil?
|
|
130
|
+
BunnyFarm::CONFIG.channel.work_pool.shutdown
|
|
131
|
+
BunnyFarm::CONFIG.channel.work_pool.kill if BunnyFarm::CONFIG.channel.work_pool.running?
|
|
132
|
+
BunnyFarm::CONFIG.channel.close
|
|
133
|
+
BunnyFarm::CONFIG.connection.close
|
|
134
|
+
end
|
|
135
|
+
end # at_exit do
|
|
136
|
+
|
|
137
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'erb'
|
|
2
|
+
require 'hashie'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
|
|
7
|
+
# TODO: need to make the location of the config directory
|
|
8
|
+
# a parameter
|
|
9
|
+
|
|
10
|
+
HIPCHAT_CONFIG = Hashie::Mash.new(
|
|
11
|
+
YAML.load(
|
|
12
|
+
ERB.new(
|
|
13
|
+
File.read(
|
|
14
|
+
File.join(JOB_CONFIG.root, 'config/hipchat.yml.erb'))).result))[JOB_CONFIG.env]
|
|
15
|
+
|
|
16
|
+
# TODO: Consider a generic Notifier base class and let
|
|
17
|
+
# HipchatNotifier inherent from it
|
|
18
|
+
class HipchatNotifier
|
|
19
|
+
|
|
20
|
+
def initialize(config)
|
|
21
|
+
|
|
22
|
+
@uri = URI.parse("https://api.hipchat.com/v2/room/#{config.room}/notification?auth_token=#{config.token}")
|
|
23
|
+
|
|
24
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
|
25
|
+
|
|
26
|
+
@http.use_ssl = true
|
|
27
|
+
|
|
28
|
+
@request = Net::HTTP::Post.new(@uri.request_uri, {'Content-Type' => 'application/json'})
|
|
29
|
+
|
|
30
|
+
end # def initialize(config)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def notify(a_message)
|
|
34
|
+
|
|
35
|
+
@request.body = {
|
|
36
|
+
"notify" => true,
|
|
37
|
+
"message_format" => "text",
|
|
38
|
+
"message" => "#{JOB_CONFIG.my_name} #{a_message}"
|
|
39
|
+
}.to_json
|
|
40
|
+
|
|
41
|
+
@http.request(@request)
|
|
42
|
+
|
|
43
|
+
end # def notify(a_message)
|
|
44
|
+
end # class HipchatNotifier
|
|
45
|
+
|
|
46
|
+
JOB_CONFIG.notifier = HipchatNotifier.new(HIPCHAT_CONFIG)
|
|
47
|
+
JOB_CONFIG.log.info('Hipchat notifier has been initialized')
|
|
48
|
+
|
|
49
|
+
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
require_relative 'message_elements'
|
|
4
|
+
|
|
5
|
+
module BunnyFarm
|
|
6
|
+
|
|
7
|
+
# SNELL: why isn't this:
|
|
8
|
+
# module Message
|
|
9
|
+
# class Base
|
|
10
|
+
# in keeping with the Rails pattern?
|
|
11
|
+
|
|
12
|
+
class Message
|
|
13
|
+
|
|
14
|
+
# contains the valid fields
|
|
15
|
+
attr_accessor :items
|
|
16
|
+
|
|
17
|
+
# contains all the junk sent in the JSON payload
|
|
18
|
+
attr_accessor :elements
|
|
19
|
+
|
|
20
|
+
# The JSON payload that was delivered
|
|
21
|
+
attr_accessor :payload
|
|
22
|
+
|
|
23
|
+
# The AMQP delivery information
|
|
24
|
+
attr_accessor :delivery_info
|
|
25
|
+
|
|
26
|
+
# SMELL: ClassName.new is being used as a dispatcher
|
|
27
|
+
def initialize(a_json_payload='', delivery_info=nil, metadata=nil)
|
|
28
|
+
success! # ass_u_me its gonna work
|
|
29
|
+
|
|
30
|
+
@payload = a_json_payload
|
|
31
|
+
@delivery_info = delivery_info
|
|
32
|
+
@metadata = metadata
|
|
33
|
+
|
|
34
|
+
if @payload.empty?
|
|
35
|
+
return(failure 'payload was empty')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if String == @payload.class
|
|
39
|
+
begin
|
|
40
|
+
@elements = MessageElements.new( JSON.parse(@payload) )
|
|
41
|
+
rescue Exception => e
|
|
42
|
+
return(failure e)
|
|
43
|
+
end
|
|
44
|
+
elsif Hash == @payload.class
|
|
45
|
+
begin
|
|
46
|
+
@elements = MessageElements.new( @payload )
|
|
47
|
+
rescue Exception => e
|
|
48
|
+
return(failure e)
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
return(failure "payload is unknown class: #{@payload.class}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
@items = @elements.tdv_extract(@@item_names)
|
|
56
|
+
rescue Exception => e
|
|
57
|
+
return(failure e)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# SMELL: is this necessary if a hash was passed in?
|
|
62
|
+
@payload = to_json unless String == @payload.class
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# NOTE: This is the message dispatcher
|
|
66
|
+
unless @delivery_info.nil?
|
|
67
|
+
params = @delivery_info.routing_key.split('.')
|
|
68
|
+
job_name = params.shift
|
|
69
|
+
action_request = params.shift.to_sym
|
|
70
|
+
|
|
71
|
+
unless job_name == self.class.to_s
|
|
72
|
+
return(failure "Routing error; wrong job name: #{job_name} Expected: #{self.class}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
unless @@allowed_actions.include?(action_request)
|
|
76
|
+
return(failure "invalid action request: #{action_request} Expected: #{@@allowed_actions.join(',')}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if successful?
|
|
80
|
+
self.send(action_request, params)
|
|
81
|
+
if CONFIG.run && CONFIG.run.channel
|
|
82
|
+
CONFIG.run.channel.acknowledge( delivery_info.delivery_tag, false ) if success?
|
|
83
|
+
CONFIG.run.channel.reject( delivery_info.delivery_tag ) if failure?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end # unless delivery_info.nil?
|
|
87
|
+
|
|
88
|
+
end # def initialize(a_json_payload, delivery_info=nil)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def publish(action='')
|
|
92
|
+
return(failure "Really? Action: '#{action.inspect}'") unless action.respond_to?(:to_s)
|
|
93
|
+
return(failure 'unspecified action') if action.empty?
|
|
94
|
+
|
|
95
|
+
action = action.to_s unless action.is_a?(String)
|
|
96
|
+
|
|
97
|
+
# NOTE: that is about all the error checking we can do in a generic
|
|
98
|
+
# sort of way. Had thought about checking against #allowed_actions
|
|
99
|
+
# but realized that there may be a different job manager coordinating
|
|
100
|
+
# different actions against the same class name.
|
|
101
|
+
|
|
102
|
+
@payload = to_json
|
|
103
|
+
|
|
104
|
+
begin
|
|
105
|
+
if CONFIG.exchange
|
|
106
|
+
CONFIG.exchange.publish(
|
|
107
|
+
@payload,
|
|
108
|
+
routing_key: "#{self.class}.#{action}",
|
|
109
|
+
app_id: CONFIG.app_id
|
|
110
|
+
)
|
|
111
|
+
else
|
|
112
|
+
failure("undefined method 'publish' for nil")
|
|
113
|
+
end
|
|
114
|
+
rescue Exception => e
|
|
115
|
+
failure(e)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
success?
|
|
119
|
+
end # def publish(action=''
|
|
120
|
+
|
|
121
|
+
# some utilities
|
|
122
|
+
|
|
123
|
+
def error(a_string)
|
|
124
|
+
@errors = [] unless @errors.is_a? Array
|
|
125
|
+
@errors << "#{self.class} #{a_string}"
|
|
126
|
+
STDERR.puts "ERROR: " + @errors.last
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def errors
|
|
130
|
+
@errors
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def to_json
|
|
134
|
+
@items.to_json
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def to_hash
|
|
138
|
+
@items
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def keys
|
|
142
|
+
@items.keys
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def [](a_key)
|
|
147
|
+
@items[a_key]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def []=(a_key,a_value)
|
|
151
|
+
@items[a_key.to_sym] = a_value
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def <<(a_hash)
|
|
155
|
+
@items.merge!(a_hash.symbolize_keys)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def each(&block)
|
|
159
|
+
@items.each(&block)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# some success/failure utilities
|
|
163
|
+
|
|
164
|
+
def successful?()
|
|
165
|
+
@processing_successful
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def success(result=true)
|
|
169
|
+
@processing_successful &&= result
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def failure(a_string='Unknown failure')
|
|
173
|
+
error a_string
|
|
174
|
+
success(false)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def success?()
|
|
178
|
+
@processing_successful
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def failure?()
|
|
182
|
+
!@processing_successful
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def success!()
|
|
186
|
+
@processing_successful = true
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def failure!(a_string='Unknown really bad failure')
|
|
190
|
+
error a_string
|
|
191
|
+
@processing_successful = false
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# A little DSL candy
|
|
195
|
+
class << self
|
|
196
|
+
|
|
197
|
+
# SMELL: These class variables... are they of ::Message or of
|
|
198
|
+
# its subclass? If ::Message then that is a problem.
|
|
199
|
+
def fields(*args)
|
|
200
|
+
@@item_names = args.flatten
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def actions(*args)
|
|
204
|
+
@@allowed_actions = args.flatten.map do |s|
|
|
205
|
+
s.is_a?(Symbol) ? s : s.to_sym
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def item_names
|
|
210
|
+
@@item_names
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def allowed_actions
|
|
214
|
+
@@allowed_actions
|
|
215
|
+
end
|
|
216
|
+
end # class << self
|
|
217
|
+
end # class Message
|
|
218
|
+
end # module BunnyFarm
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative 'hash_ext'
|
|
2
|
+
|
|
3
|
+
class MessageElements < Hash
|
|
4
|
+
|
|
5
|
+
def initialize(*args)
|
|
6
|
+
|
|
7
|
+
super()
|
|
8
|
+
|
|
9
|
+
# SMELL: flatten _assumes_ that there is no structure
|
|
10
|
+
# to the JSON message being handled. Why did I
|
|
11
|
+
# do it this way? Might have been to ensure that
|
|
12
|
+
# all the hash keys were symbolized. I need a
|
|
13
|
+
# deep symbolize_keys.
|
|
14
|
+
|
|
15
|
+
if args.size > 0
|
|
16
|
+
args.flatten.each do |a|
|
|
17
|
+
if a.is_a? Hash
|
|
18
|
+
self.merge!(a.deep_symbolize_keys)
|
|
19
|
+
else
|
|
20
|
+
self[a.to_sym]=nil
|
|
21
|
+
end
|
|
22
|
+
end # args.flatten.each do |a|
|
|
23
|
+
end # if args.size > 0
|
|
24
|
+
end # def initialize(*args)
|
|
25
|
+
end # class MessageElements < Hash
|