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.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/docs.yml +38 -0
  4. data/.gitignore +11 -0
  5. data/.travis.yml +3 -0
  6. data/CHANGELOG.md +61 -0
  7. data/COMMITS.md +196 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +330 -0
  11. data/Rakefile +9 -0
  12. data/bunny_farm.gemspec +30 -0
  13. data/config/bunny.yml.erb +29 -0
  14. data/config/bunny_test.yml.erb +29 -0
  15. data/config/hipchat.yml.erb +12 -0
  16. data/docs/api/configuration.md +9 -0
  17. data/docs/api/consumer.md +8 -0
  18. data/docs/api/message-class.md +419 -0
  19. data/docs/api/publisher.md +9 -0
  20. data/docs/architecture/integration.md +8 -0
  21. data/docs/architecture/message-flow.md +11 -0
  22. data/docs/architecture/overview.md +448 -0
  23. data/docs/architecture/scaling.md +8 -0
  24. data/docs/assets/actions_dsl_flow.svg +109 -0
  25. data/docs/assets/architecture_overview.svg +152 -0
  26. data/docs/assets/best_practices_patterns.svg +203 -0
  27. data/docs/assets/bunny_farm_logo.png +0 -0
  28. data/docs/assets/configuration_api_methods.svg +104 -0
  29. data/docs/assets/configuration_flow.svg +130 -0
  30. data/docs/assets/configuration_hierarchy.svg +70 -0
  31. data/docs/assets/data_processing_pipeline.svg +131 -0
  32. data/docs/assets/debugging_monitoring.svg +165 -0
  33. data/docs/assets/ecommerce_example_flow.svg +145 -0
  34. data/docs/assets/email_campaign_example.svg +127 -0
  35. data/docs/assets/environment_variables_map.svg +78 -0
  36. data/docs/assets/error_handling_flow.svg +114 -0
  37. data/docs/assets/favicon.ico +1 -0
  38. data/docs/assets/fields_dsl_structure.svg +89 -0
  39. data/docs/assets/instance_methods_lifecycle.svg +137 -0
  40. data/docs/assets/integration_patterns.svg +207 -0
  41. data/docs/assets/json_serialization_flow.svg +153 -0
  42. data/docs/assets/logo.svg +4 -0
  43. data/docs/assets/message_api_overview.svg +126 -0
  44. data/docs/assets/message_encapsulation.svg +113 -0
  45. data/docs/assets/message_lifecycle.svg +110 -0
  46. data/docs/assets/message_structure.svg +138 -0
  47. data/docs/assets/publisher_consumer_api.svg +120 -0
  48. data/docs/assets/scaling_deployment_patterns.svg +195 -0
  49. data/docs/assets/smart_routing_diagram.svg +131 -0
  50. data/docs/assets/system_architecture_overview.svg +155 -0
  51. data/docs/assets/task_scheduling_flow.svg +139 -0
  52. data/docs/assets/testing_strategies.svg +146 -0
  53. data/docs/assets/workflow_patterns.svg +183 -0
  54. data/docs/assets/yaml_config_structure.svg +72 -0
  55. data/docs/configuration/environment-variables.md +14 -0
  56. data/docs/configuration/overview.md +373 -0
  57. data/docs/configuration/programmatic-setup.md +10 -0
  58. data/docs/configuration/yaml-configuration.md +12 -0
  59. data/docs/core-features/configuration.md +528 -0
  60. data/docs/core-features/error-handling.md +82 -0
  61. data/docs/core-features/json-serialization.md +545 -0
  62. data/docs/core-features/message-design.md +406 -0
  63. data/docs/core-features/smart-routing.md +467 -0
  64. data/docs/core-features/task-scheduling.md +67 -0
  65. data/docs/core-features/workflow-support.md +112 -0
  66. data/docs/development/contributing.md +345 -0
  67. data/docs/development/roadmap.md +9 -0
  68. data/docs/development/testing.md +14 -0
  69. data/docs/examples/order-processing.md +10 -0
  70. data/docs/examples/overview.md +269 -0
  71. data/docs/examples/real-world.md +8 -0
  72. data/docs/examples/simple-producer-consumer.md +15 -0
  73. data/docs/examples/task-scheduler.md +9 -0
  74. data/docs/getting-started/basic-concepts.md +274 -0
  75. data/docs/getting-started/installation.md +122 -0
  76. data/docs/getting-started/quick-start.md +158 -0
  77. data/docs/index.md +106 -0
  78. data/docs/message-structure/actions-dsl.md +163 -0
  79. data/docs/message-structure/fields-dsl.md +146 -0
  80. data/docs/message-structure/instance-methods.md +115 -0
  81. data/docs/message-structure/overview.md +211 -0
  82. data/examples/README.md +212 -0
  83. data/examples/consumer.rb +41 -0
  84. data/examples/images/message_flow.svg +87 -0
  85. data/examples/images/order_workflow.svg +122 -0
  86. data/examples/images/producer_consumer.svg +96 -0
  87. data/examples/images/task_scheduler.svg +140 -0
  88. data/examples/order_processor.rb +238 -0
  89. data/examples/producer.rb +60 -0
  90. data/examples/simple_message.rb +43 -0
  91. data/examples/task_scheduler.rb +263 -0
  92. data/images/architecture_overview.svg +152 -0
  93. data/images/bunny_farm_logo.png +0 -0
  94. data/images/configuration_flow.svg +130 -0
  95. data/images/message_structure.svg +138 -0
  96. data/lib/bunny_farm/.irbrc +7 -0
  97. data/lib/bunny_farm/generic_consumer.rb +12 -0
  98. data/lib/bunny_farm/hash_ext.rb +37 -0
  99. data/lib/bunny_farm/init_bunny.rb +137 -0
  100. data/lib/bunny_farm/init_hipchat.rb +49 -0
  101. data/lib/bunny_farm/message.rb +218 -0
  102. data/lib/bunny_farm/message_elements.rb +25 -0
  103. data/lib/bunny_farm/version.rb +3 -0
  104. data/lib/bunny_farm.rb +9 -0
  105. data/mkdocs.yml +148 -0
  106. 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 &lt; 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 &amp; 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,7 @@
1
+ require 'init_bunny.rb'
2
+ $debug = false
3
+
4
+ BunnyFarm.config do
5
+ xyz 123
6
+ end
7
+
@@ -0,0 +1,12 @@
1
+ module BunnyFarm
2
+ class GenericConsumer < Bunny::Consumer
3
+
4
+ def cancelled?
5
+ @cancelled
6
+ end
7
+
8
+ def handle_cancellation(_)
9
+ @cancelled = true
10
+ end
11
+ end # class GenericConsumer < Bunny::Consumer
12
+ end # module BunnyFarm
@@ -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
@@ -0,0 +1,3 @@
1
+ module BunnyFarm
2
+ VERSION = '0.1.2'
3
+ end
data/lib/bunny_farm.rb ADDED
@@ -0,0 +1,9 @@
1
+ # bunny_farm.rb
2
+
3
+ require "bunny_farm/version"
4
+ require "bunny_farm/message"
5
+ require "bunny_farm/init_bunny"
6
+
7
+ module BunnyFarm
8
+ # BunnyFarm: where rabbits are planted ears up
9
+ end