hoodoo 1.0.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 (216) hide show
  1. checksums.yaml +7 -0
  2. data/bin/hoodoo +5 -0
  3. data/lib/hoodoo.rb +27 -0
  4. data/lib/hoodoo/active.rb +32 -0
  5. data/lib/hoodoo/active/active_model/uuid_validator.rb +45 -0
  6. data/lib/hoodoo/active/active_record/base.rb +81 -0
  7. data/lib/hoodoo/active/active_record/creator.rb +134 -0
  8. data/lib/hoodoo/active/active_record/dated.rb +343 -0
  9. data/lib/hoodoo/active/active_record/error_mapping.rb +351 -0
  10. data/lib/hoodoo/active/active_record/finder.rb +606 -0
  11. data/lib/hoodoo/active/active_record/search_helper.rb +189 -0
  12. data/lib/hoodoo/active/active_record/secure.rb +431 -0
  13. data/lib/hoodoo/active/active_record/support.rb +106 -0
  14. data/lib/hoodoo/active/active_record/translated.rb +87 -0
  15. data/lib/hoodoo/active/active_record/uuid.rb +80 -0
  16. data/lib/hoodoo/active/active_record/writer.rb +321 -0
  17. data/lib/hoodoo/client.rb +23 -0
  18. data/lib/hoodoo/client/augmented_array.rb +29 -0
  19. data/lib/hoodoo/client/augmented_base.rb +168 -0
  20. data/lib/hoodoo/client/augmented_hash.rb +23 -0
  21. data/lib/hoodoo/client/client.rb +354 -0
  22. data/lib/hoodoo/client/endpoint/endpoint.rb +427 -0
  23. data/lib/hoodoo/client/endpoint/endpoints/amqp.rb +180 -0
  24. data/lib/hoodoo/client/endpoint/endpoints/auto_session.rb +194 -0
  25. data/lib/hoodoo/client/endpoint/endpoints/http.rb +203 -0
  26. data/lib/hoodoo/client/endpoint/endpoints/http_based.rb +367 -0
  27. data/lib/hoodoo/client/endpoint/endpoints/not_found.rb +59 -0
  28. data/lib/hoodoo/client/headers.rb +269 -0
  29. data/lib/hoodoo/communicators.rb +23 -0
  30. data/lib/hoodoo/communicators/fast.rb +44 -0
  31. data/lib/hoodoo/communicators/pool.rb +601 -0
  32. data/lib/hoodoo/communicators/slow.rb +84 -0
  33. data/lib/hoodoo/data.rb +51 -0
  34. data/lib/hoodoo/data/resources/caller.rb +39 -0
  35. data/lib/hoodoo/data/resources/errors.rb +28 -0
  36. data/lib/hoodoo/data/resources/log.rb +31 -0
  37. data/lib/hoodoo/data/resources/session.rb +26 -0
  38. data/lib/hoodoo/data/types/error_primitive.rb +27 -0
  39. data/lib/hoodoo/data/types/permissions.rb +40 -0
  40. data/lib/hoodoo/data/types/permissions_defaults.rb +32 -0
  41. data/lib/hoodoo/data/types/permissions_full.rb +28 -0
  42. data/lib/hoodoo/data/types/permissions_resources.rb +31 -0
  43. data/lib/hoodoo/discovery.rb +20 -0
  44. data/lib/hoodoo/errors.rb +19 -0
  45. data/lib/hoodoo/errors/error_descriptions.rb +229 -0
  46. data/lib/hoodoo/errors/errors.rb +322 -0
  47. data/lib/hoodoo/generator.rb +139 -0
  48. data/lib/hoodoo/logger.rb +23 -0
  49. data/lib/hoodoo/logger/fast_writer.rb +27 -0
  50. data/lib/hoodoo/logger/flattener_mixin.rb +36 -0
  51. data/lib/hoodoo/logger/logger.rb +387 -0
  52. data/lib/hoodoo/logger/slow_writer.rb +49 -0
  53. data/lib/hoodoo/logger/writer_mixin.rb +52 -0
  54. data/lib/hoodoo/logger/writers/file_writer.rb +45 -0
  55. data/lib/hoodoo/logger/writers/log_entries_dot_com_writer.rb +64 -0
  56. data/lib/hoodoo/logger/writers/stream_writer.rb +43 -0
  57. data/lib/hoodoo/middleware.rb +33 -0
  58. data/lib/hoodoo/presenters.rb +45 -0
  59. data/lib/hoodoo/presenters/base.rb +281 -0
  60. data/lib/hoodoo/presenters/base_dsl.rb +519 -0
  61. data/lib/hoodoo/presenters/common_resource_fields.rb +31 -0
  62. data/lib/hoodoo/presenters/embedding.rb +232 -0
  63. data/lib/hoodoo/presenters/types/array.rb +118 -0
  64. data/lib/hoodoo/presenters/types/boolean.rb +26 -0
  65. data/lib/hoodoo/presenters/types/date.rb +26 -0
  66. data/lib/hoodoo/presenters/types/date_time.rb +26 -0
  67. data/lib/hoodoo/presenters/types/decimal.rb +47 -0
  68. data/lib/hoodoo/presenters/types/enum.rb +55 -0
  69. data/lib/hoodoo/presenters/types/field.rb +158 -0
  70. data/lib/hoodoo/presenters/types/float.rb +26 -0
  71. data/lib/hoodoo/presenters/types/hash.rb +361 -0
  72. data/lib/hoodoo/presenters/types/integer.rb +26 -0
  73. data/lib/hoodoo/presenters/types/object.rb +117 -0
  74. data/lib/hoodoo/presenters/types/string.rb +53 -0
  75. data/lib/hoodoo/presenters/types/tags.rb +24 -0
  76. data/lib/hoodoo/presenters/types/text.rb +26 -0
  77. data/lib/hoodoo/presenters/types/uuid.rb +54 -0
  78. data/lib/hoodoo/services.rb +34 -0
  79. data/lib/hoodoo/services/discovery/discoverers/by_consul.rb +66 -0
  80. data/lib/hoodoo/services/discovery/discoverers/by_convention.rb +173 -0
  81. data/lib/hoodoo/services/discovery/discoverers/by_drb/by_drb.rb +195 -0
  82. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server.rb +166 -0
  83. data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server_start.rb +37 -0
  84. data/lib/hoodoo/services/discovery/discovery.rb +186 -0
  85. data/lib/hoodoo/services/discovery/results/for_amqp.rb +58 -0
  86. data/lib/hoodoo/services/discovery/results/for_http.rb +85 -0
  87. data/lib/hoodoo/services/discovery/results/for_local.rb +85 -0
  88. data/lib/hoodoo/services/discovery/results/for_remote.rb +57 -0
  89. data/lib/hoodoo/services/middleware/amqp_log_message.rb +186 -0
  90. data/lib/hoodoo/services/middleware/amqp_log_writer.rb +119 -0
  91. data/lib/hoodoo/services/middleware/endpoints/inter_resource_local.rb +130 -0
  92. data/lib/hoodoo/services/middleware/endpoints/inter_resource_remote.rb +202 -0
  93. data/lib/hoodoo/services/middleware/exception_reporting/base_reporter.rb +105 -0
  94. data/lib/hoodoo/services/middleware/exception_reporting/exception_reporting.rb +115 -0
  95. data/lib/hoodoo/services/middleware/exception_reporting/reporters/airbrake_reporter.rb +64 -0
  96. data/lib/hoodoo/services/middleware/exception_reporting/reporters/raygun_reporter.rb +63 -0
  97. data/lib/hoodoo/services/middleware/interaction.rb +127 -0
  98. data/lib/hoodoo/services/middleware/middleware.rb +2705 -0
  99. data/lib/hoodoo/services/middleware/rack_monkey_patch.rb +73 -0
  100. data/lib/hoodoo/services/services/context.rb +153 -0
  101. data/lib/hoodoo/services/services/implementation.rb +132 -0
  102. data/lib/hoodoo/services/services/interface.rb +934 -0
  103. data/lib/hoodoo/services/services/permissions.rb +250 -0
  104. data/lib/hoodoo/services/services/request.rb +189 -0
  105. data/lib/hoodoo/services/services/response.rb +316 -0
  106. data/lib/hoodoo/services/services/service.rb +141 -0
  107. data/lib/hoodoo/services/services/session.rb +729 -0
  108. data/lib/hoodoo/utilities.rb +12 -0
  109. data/lib/hoodoo/utilities/string_inquirer.rb +54 -0
  110. data/lib/hoodoo/utilities/utilities.rb +380 -0
  111. data/lib/hoodoo/utilities/uuid.rb +44 -0
  112. data/lib/hoodoo/version.rb +17 -0
  113. data/spec/active/active_record/base_spec.rb +57 -0
  114. data/spec/active/active_record/creator_spec.rb +88 -0
  115. data/spec/active/active_record/dated_spec.rb +248 -0
  116. data/spec/active/active_record/error_mapping_spec.rb +360 -0
  117. data/spec/active/active_record/finder_spec.rb +744 -0
  118. data/spec/active/active_record/search_helper_spec.rb +384 -0
  119. data/spec/active/active_record/secure_spec.rb +435 -0
  120. data/spec/active/active_record/support_spec.rb +225 -0
  121. data/spec/active/active_record/translated_spec.rb +19 -0
  122. data/spec/active/active_record/uuid_spec.rb +72 -0
  123. data/spec/active/active_record/writer_spec.rb +272 -0
  124. data/spec/alchemy/alchemy-amq.rb +33 -0
  125. data/spec/client/augmented_array_spec.rb +15 -0
  126. data/spec/client/augmented_base_spec.rb +50 -0
  127. data/spec/client/augmented_hash_spec.rb +15 -0
  128. data/spec/client/client_spec.rb +955 -0
  129. data/spec/client/endpoint/endpoint_spec.rb +70 -0
  130. data/spec/client/endpoint/endpoints/amqp_spec.rb +16 -0
  131. data/spec/client/endpoint/endpoints/auto_session_spec.rb +9 -0
  132. data/spec/client/endpoint/endpoints/http_based_spec.rb +9 -0
  133. data/spec/client/endpoint/endpoints/http_spec.rb +103 -0
  134. data/spec/client/endpoint/endpoints/not_found_spec.rb +35 -0
  135. data/spec/client/headers_spec.rb +172 -0
  136. data/spec/communicators/fast_spec.rb +9 -0
  137. data/spec/communicators/pool_spec.rb +339 -0
  138. data/spec/communicators/slow_spec.rb +15 -0
  139. data/spec/data/resources/caller_spec.rb +156 -0
  140. data/spec/data/resources/errors_spec.rb +22 -0
  141. data/spec/data/resources/log_spec.rb +20 -0
  142. data/spec/data/resources/session_spec.rb +15 -0
  143. data/spec/data/types/error_primitive_spec.rb +15 -0
  144. data/spec/data/types/permissions_defaults_spec.rb +25 -0
  145. data/spec/data/types/permissions_full_spec.rb +44 -0
  146. data/spec/data/types/permissions_resources_spec.rb +34 -0
  147. data/spec/data/types/permissions_spec.rb +37 -0
  148. data/spec/errors/error_descriptions_spec.rb +98 -0
  149. data/spec/errors/errors_spec.rb +346 -0
  150. data/spec/integration/service_actions_spec.rb +112 -0
  151. data/spec/logger/fast_writer_spec.rb +18 -0
  152. data/spec/logger/logger_spec.rb +259 -0
  153. data/spec/logger/slow_writer_spec.rb +144 -0
  154. data/spec/logger/writers/file_writer_spec.rb +37 -0
  155. data/spec/logger/writers/log_entries_dot_com_writer_spec.rb +29 -0
  156. data/spec/logger/writers/stream_writer_spec.rb +38 -0
  157. data/spec/presenters/base_dsl_spec.rb +111 -0
  158. data/spec/presenters/base_spec.rb +871 -0
  159. data/spec/presenters/common_resource_fields_spec.rb +30 -0
  160. data/spec/presenters/embedding_spec.rb +87 -0
  161. data/spec/presenters/types/array_spec.rb +249 -0
  162. data/spec/presenters/types/boolean_spec.rb +51 -0
  163. data/spec/presenters/types/date_spec.rb +57 -0
  164. data/spec/presenters/types/date_time_spec.rb +59 -0
  165. data/spec/presenters/types/decimal_spec.rb +58 -0
  166. data/spec/presenters/types/enum_spec.rb +71 -0
  167. data/spec/presenters/types/field_spec.rb +77 -0
  168. data/spec/presenters/types/float_spec.rb +50 -0
  169. data/spec/presenters/types/hash_spec.rb +1069 -0
  170. data/spec/presenters/types/integer_spec.rb +50 -0
  171. data/spec/presenters/types/object_spec.rb +177 -0
  172. data/spec/presenters/types/string_spec.rb +65 -0
  173. data/spec/presenters/types/tags_spec.rb +56 -0
  174. data/spec/presenters/types/text_spec.rb +50 -0
  175. data/spec/presenters/types/uuid_spec.rb +46 -0
  176. data/spec/presenters/walk_spec.rb +198 -0
  177. data/spec/services/discovery/discoverers/by_consul_spec.rb +29 -0
  178. data/spec/services/discovery/discoverers/by_convention_spec.rb +67 -0
  179. data/spec/services/discovery/discoverers/by_drb/by_drb_spec.rb +80 -0
  180. data/spec/services/discovery/discoverers/by_drb/drb_server_spec.rb +205 -0
  181. data/spec/services/discovery/discovery_spec.rb +73 -0
  182. data/spec/services/discovery/results/for_amqp_spec.rb +17 -0
  183. data/spec/services/discovery/results/for_http_spec.rb +37 -0
  184. data/spec/services/discovery/results/for_local_spec.rb +21 -0
  185. data/spec/services/discovery/results/for_remote_spec.rb +15 -0
  186. data/spec/services/middleware/amqp_log_message_spec.rb +60 -0
  187. data/spec/services/middleware/amqp_log_writer_spec.rb +95 -0
  188. data/spec/services/middleware/endpoints/inter_resource_local_spec.rb +9 -0
  189. data/spec/services/middleware/endpoints/inter_resource_remote_spec.rb +9 -0
  190. data/spec/services/middleware/exception_reporting/base_reporter_spec.rb +16 -0
  191. data/spec/services/middleware/exception_reporting/exception_reporting_spec.rb +92 -0
  192. data/spec/services/middleware/exception_reporting/reporters/airbrake_reporter_spec.rb +24 -0
  193. data/spec/services/middleware/exception_reporting/reporters/raygun_reporter_spec.rb +23 -0
  194. data/spec/services/middleware/middleware_cors_spec.rb +93 -0
  195. data/spec/services/middleware/middleware_create_update_spec.rb +489 -0
  196. data/spec/services/middleware/middleware_dated_at_spec.rb +186 -0
  197. data/spec/services/middleware/middleware_exotic_communication_spec.rb +560 -0
  198. data/spec/services/middleware/middleware_logging_spec.rb +356 -0
  199. data/spec/services/middleware/middleware_multi_local_spec.rb +1094 -0
  200. data/spec/services/middleware/middleware_multi_remote_spec.rb +1440 -0
  201. data/spec/services/middleware/middleware_permissions_spec.rb +1014 -0
  202. data/spec/services/middleware/middleware_public_spec.rb +238 -0
  203. data/spec/services/middleware/middleware_spec.rb +1569 -0
  204. data/spec/services/middleware/string_inquirer_spec.rb +30 -0
  205. data/spec/services/services/application_spec.rb +74 -0
  206. data/spec/services/services/context_spec.rb +48 -0
  207. data/spec/services/services/implementation_spec.rb +45 -0
  208. data/spec/services/services/interface_spec.rb +262 -0
  209. data/spec/services/services/permissions_spec.rb +249 -0
  210. data/spec/services/services/request_spec.rb +95 -0
  211. data/spec/services/services/response_spec.rb +250 -0
  212. data/spec/services/services/session_spec.rb +432 -0
  213. data/spec/spec_helper.rb +298 -0
  214. data/spec/utilities/utilities_spec.rb +537 -0
  215. data/spec/utilities/uuid_spec.rb +20 -0
  216. metadata +615 -0
@@ -0,0 +1,59 @@
1
+
2
+ module Hoodoo
3
+ class Client # Just used as a namespace here
4
+ class Endpoint # Just used as a namespace here
5
+
6
+ # An endpoint that, when called, returns 'Not Found' for
7
+ # the resource at hand. Used to emulate lazily resolved
8
+ # endpoints when in fact, the lack of endpoint presence
9
+ # is already known.
10
+ #
11
+ # Ignores any discovery result data if provided.
12
+ #
13
+ class NotFound < Hoodoo::Client::Endpoint::HTTPBased
14
+
15
+ protected
16
+
17
+ # See Hoodoo::Client::Endpoint#configure_with.
18
+ #
19
+ # Does nothing in this subclass.
20
+ #
21
+ def configure_with( resource, version, options )
22
+ end
23
+
24
+ public
25
+
26
+ # See Hoodoo::Client::Endpoint#list.
27
+ #
28
+ def list( query_hash = nil )
29
+ return generate_404_response_for( :list )
30
+ end
31
+
32
+ # See Hoodoo::Client::Endpoint#show.
33
+ #
34
+ def show( ident, query_hash = nil )
35
+ return generate_404_response_for( :show )
36
+ end
37
+
38
+ # See Hoodoo::Client::Endpoint#create.
39
+ #
40
+ def create( body_hash, query_hash = nil )
41
+ return generate_404_response_for( :create )
42
+ end
43
+
44
+ # See Hoodoo::Client::Endpoint#update.
45
+ #
46
+ def update( ident, body_hash, query_hash = nil )
47
+ return generate_404_response_for( :update )
48
+ end
49
+
50
+ # See Hoodoo::Client::Endpoint#delete.
51
+ #
52
+ def delete( ident, query_hash = nil )
53
+ return generate_404_response_for( :delete )
54
+ end
55
+
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,269 @@
1
+ ########################################################################
2
+ # File:: headers.rb
3
+ # (C):: Loyalty New Zealand 2015
4
+ #
5
+ # Purpose:: If you can't think of where some code should live, use a
6
+ # Class or Module as a namespace for what amounts to library
7
+ # routines. The class defined here has support data and code
8
+ # for Hoodoo::Client, Hoodoo::Services::Middleware and others.
9
+ # ----------------------------------------------------------------------
10
+ # 22-Sep-2015 (ADH): Created.
11
+ ########################################################################
12
+
13
+ module Hoodoo
14
+ class Client
15
+
16
+ # Hoodoo::Client and related software such as
17
+ # Hoodoo::Services::Middleware need common access to information about
18
+ # special processing headers defined by Hoodoo and the Hoodoo API. This
19
+ # class is just a container - pretty much a namespaced library - holding
20
+ # that kind of information and support methods.
21
+ #
22
+ class Headers
23
+
24
+ # Used by HEADER_TO_PROPERTY; this Proc when called with some non-nil
25
+ # value from an HTTP header representing a UUID, evaluates to either the
26
+ # UUID as a String or +nil+ if the value appeared to not be a UUID.
27
+ #
28
+ UUID_PROPERTY_PROC = -> ( value ) {
29
+ value = Hoodoo::UUID.valid?( value ) && value
30
+ value || nil # => 'value' if 'value' is truthy, 'nil' if 'value' falsy
31
+ }
32
+
33
+ # Used by HEADER_TO_PROPERTY; this Proc when called with some UUID
34
+ # evaluates to the input value coerced to a String and no other changes.
35
+ #
36
+ UUID_HEADER_PROC = -> ( value ) { value }
37
+
38
+ # Used by HEADER_TO_PROPERTY; this Proc when called with some non-nil
39
+ # value from an HTTP header representing a Date/Time in a supported
40
+ # format, evaluates to either a parsed DateTime instance or +nil+ if the
41
+ # value appeared to not be in a supported format.
42
+ #
43
+ DATETIME_IN_PAST_ONLY_PROPERTY_PROC = -> ( value ) {
44
+ value = Hoodoo::Utilities.valid_iso8601_subset_datetime?( value )
45
+ value = nil if value && value > DateTime.now
46
+ value || nil # => 'value' if 'value' is truthy, 'nil' if 'value' falsy
47
+ }
48
+
49
+ # Used by HEADER_TO_PROPERTY; this Proc is called with a Time, Date,
50
+ # DateTime or DateTime-parseable String and returns a DateTime. It is
51
+ # used for a custom write accessor for the property associated with
52
+ # a header entry and works independently of the validation mechanism
53
+ # for inbound String-only from-header data.
54
+ #
55
+ DATETIME_WRITER_PROC = -> ( value ) { Hoodoo::Utilities.rationalise_datetime( value ) }
56
+
57
+ # Used by HEADER_TO_PROPERTY; this Proc when called with a DateTime
58
+ # instance evaluates to a String representing the DateTime as an
59
+ # ISO 8601 subset value given to nanosecond precision.
60
+ #
61
+ DATETIME_HEADER_PROC = -> ( value ) { Hoodoo::Utilities.nanosecond_iso8601( value ) }
62
+
63
+ # Used by HEADER_TO_PROPERTY; this Proc when called with some non-nil
64
+ # value from an HTTP header representing a Boolean as "yes" or "no",
65
+ # evaluates to either +true+ for "yes" or +false+ for any other value.
66
+ # Case insensitive.
67
+ #
68
+ BOOLEAN_PROPERTY_PROC = -> ( value ) {
69
+ value.to_s.downcase == 'yes' || value == true ? true : false
70
+ }
71
+
72
+ # Used by HEADER_TO_PROPERTY; this Proc when called with +true+ or
73
+ # +false+ evaluates to String "yes" for +true+ or "no" for any other
74
+ # value.
75
+ #
76
+ BOOLEAN_HEADER_PROC = -> ( value ) { value == true ? 'yes' : 'no' }
77
+
78
+ # Various "X-Foo"-style HTTP headers specified in the Hoodoo API
79
+ # Specification have special meanings and values for those need to be
80
+ # set up in request data and Hoodoo::Client endpoints. Processing
81
+ # around these is data driven by this mapping Hash.
82
+ #
83
+ # Keys are the HTTP header names in Rack (upper case, "HTTP_"-prefix)
84
+ # format. Values are options bundles as follows:
85
+ #
86
+ # +property+:: The property name to be associated with the header,
87
+ # as a Symbol.
88
+ #
89
+ # +property_proc+:: A Proc that's called to both validate and clean up
90
+ # the raw value from the HTTP header. It evaluates to
91
+ # +nil+ if the value is invalid, or non-+nil+ for any
92
+ # other case. Note that there is no way for an HTTP
93
+ # header to explicitly convey a corresponding value
94
+ # internally of +nil+ as a result, by design; instead
95
+ # the relevant header would simply be omitted by the
96
+ # caller (and/or change your header design!).
97
+ #
98
+ # +writer_proc+:: If a property has a possible amigbuity of input
99
+ # data types when set externally, independently of
100
+ # any validation etc. from the +property_proc+
101
+ # option, then this optional entry contains a Proc
102
+ # that is used for a custom write accessor and
103
+ # canonicalises assumed-valid but possibly not
104
+ # canonical input for writing. An example would be
105
+ # the conversion of String or Time instances to a
106
+ # DateTime so that a property always reads back with
107
+ # a DateTime instance.
108
+ #
109
+ # +header+:: For speed in lookups where it's needed, this is the
110
+ # "real" (not Rack format) HTTP header name.
111
+ #
112
+ # +header_proc+:: A Proc that's called to convert a cleaned-up value
113
+ # set in the +property+ by its +property_proc+. It
114
+ # is called with this value and returns an equivalent
115
+ # appropriate value for use with the HTTP header
116
+ # given in +header+. This _MUST_ always be a String.
117
+ #
118
+ # +secured+:: Optional, default +nil+. If +true+, marks that
119
+ # this header and its associated value can only be
120
+ # processed if there is a Session with a Caller that
121
+ # has an +authorised_http_headers+ entry for this
122
+ # header.
123
+ #
124
+ # +auto_transfer+:: Optional, default +nil+. Only relevant to
125
+ # inter-resource call scenarios. If +true+, when one
126
+ # resource calls another, the value of this property
127
+ # is automatically transferred to the downstream
128
+ # resource. Otherwise, it is not, and the downstream
129
+ # resource will operate under whatever defaults are
130
+ # present. An inter-resource call endpoint which
131
+ # inherits an auto-transfer property can always have
132
+ # this property explicitly overwritten before any
133
+ # calls are made through it.
134
+ #
135
+ # An additional key of +:property_writer+ will be set up automatically
136
+ # which contains the value of the +:property+ key with an "=" sign added,
137
+ # resulting in the name of a write accessor method for that property.
138
+ #
139
+ HEADER_TO_PROPERTY =
140
+ {
141
+ # Take care not to define any property name which clashes with an
142
+ # option in any other part of this entire system where these "other
143
+ # options" get merged in. A project search for
144
+ # 'HEADER_TO_PROPERTY' in comments should find those.
145
+
146
+ 'HTTP_X_RESOURCE_UUID' => {
147
+ :property => :resource_uuid,
148
+ :property_proc => UUID_PROPERTY_PROC,
149
+ :header => 'X-Resource-UUID',
150
+ :header_proc => UUID_HEADER_PROC,
151
+
152
+ :secured => true,
153
+ },
154
+
155
+ 'HTTP_X_DATED_AT' => {
156
+ :property => :dated_at,
157
+ :property_proc => DATETIME_IN_PAST_ONLY_PROPERTY_PROC,
158
+ :writer_proc => DATETIME_WRITER_PROC,
159
+ :header => 'X-Dated-At',
160
+ :header_proc => DATETIME_HEADER_PROC,
161
+
162
+ :auto_transfer => true,
163
+ },
164
+
165
+ 'HTTP_X_DATED_FROM' => {
166
+ :property => :dated_from,
167
+ :property_proc => DATETIME_IN_PAST_ONLY_PROPERTY_PROC,
168
+ :writer_proc => DATETIME_WRITER_PROC,
169
+ :header => 'X-Dated-From',
170
+ :header_proc => DATETIME_HEADER_PROC,
171
+
172
+ :auto_transfer => true,
173
+ },
174
+
175
+ 'HTTP_X_DEJA_VU' => {
176
+ :property => :deja_vu,
177
+ :property_proc => BOOLEAN_PROPERTY_PROC,
178
+ :header => 'X-Deja-Vu',
179
+ :header_proc => BOOLEAN_HEADER_PROC,
180
+ },
181
+ }
182
+
183
+ # For speed, fill in a "property_writer" value, where "foo" becomes
184
+ # "foo=" - otherwise this has to be done in lots of speed-sensitive
185
+ # code sections.
186
+ #
187
+ HEADER_TO_PROPERTY.each_value do | value |
188
+ value[ :property_writer ] = "#{ value[ :property ] }="
189
+ end
190
+
191
+ # Define a series of read and custom write accessors according to the
192
+ # HTTP_HEADER_OPTIONS_MAP. For example, a property of "dated_at" results
193
+ # in a <tt>dated_at</tt> reader, a <tt>dated_at=</tt> writer which calls
194
+ # Hoodoo::Utilities.rationalise_datetime to clean up the input value
195
+ # and sets the result into the <tt>@dated_at</tt> instance variable which
196
+ # the read accessor will be expecting to use.
197
+ #
198
+ # +klass+:: The Class to which the instance methods will be added.
199
+ #
200
+ def self.define_accessors_for_header_equivalents( klass )
201
+ klass.class_eval do
202
+ HEADER_TO_PROPERTY.each do | rack_header, description |
203
+ attr_reader( description[ :property ] )
204
+
205
+ custom_writer = description[ :writer_proc ]
206
+
207
+ if custom_writer.nil?
208
+ attr_writer( description[ :property ] )
209
+ else
210
+ define_method( "#{ description[ :property ] }=" ) do | parameter |
211
+ instance_variable_set(
212
+ "@#{ description[ :property ] }",
213
+ description[ :writer_proc ].call( parameter )
214
+ )
215
+ result = instance_variable_get("@#{ description[ :property ] }")
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ # From a Hash-like source where keys are HTTP header names and values
223
+ # are the corresponding HTTP header values, extract interesting values
224
+ # and return a Hash of options as described below.
225
+ #
226
+ # Any <tt>X-Foo</tt> header is extracted, including core Hoodoo extension
227
+ # headers such as <tt>X-Interaction-ID</tt>, which is present in any
228
+ # response. The "X-" is stripped, the rest converted to lower case and
229
+ # hyphens converted to underscores. The interaction ID, therefore, would
230
+ # be set as an +interaction_id+ option. <tt>X-Foo</tt> would be set as a
231
+ # +foo+ option - and so-on.
232
+ #
233
+ # The header matcher accepts headers from the Hash-like source in upper
234
+ # or lower case with hyphens or underscores inside; extracted headers can
235
+ # therefore start with any of <tt>X_</tt>, <tt>x_</tt>, <tt>X-</tt> or
236
+ # <tt>x-</tt>. The Hash-like source must support the +each+ operator
237
+ # yielding a key and value to the block on each iteration.
238
+ #
239
+ # Header values are not translated at all, so (unless something very
240
+ # unsual is going on) the option values will be Strings.
241
+ #
242
+ # If the same header is encountered more than once, only the first one
243
+ # encountered (in enumeration order, whatever that might be) is stored.
244
+ #
245
+ # Parameters:
246
+ #
247
+ # +hashlike_source+:: Hash-like source containing HTTP headers/values.
248
+ #
249
+ def self.x_header_to_options( hashlike_source )
250
+ hashlike_source ||= {}
251
+ options = {}
252
+
253
+ hashlike_source.each do | key, value |
254
+ next unless ( key[ 0 ] == 'x' || key[ 0 ] == 'X' ) &&
255
+ ( key[ 1 ] == '-' || key[ 1 ] == '_' )
256
+
257
+ entry = key.to_s.downcase.gsub( '-', '_' )[ 2..-1 ]
258
+
259
+ unless entry == '' || options.has_key?( entry )
260
+ options[ entry ] = value
261
+ end
262
+ end
263
+
264
+ return options
265
+ end
266
+
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,23 @@
1
+ ########################################################################
2
+ # File:: communicators.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: Include the code providing a pool of fast or slow workers
6
+ # that communicate with the outside world.
7
+ # ----------------------------------------------------------------------
8
+ # 26-Jan-2015 (ADH): Split from top-level inclusion file.
9
+ ########################################################################
10
+
11
+ module Hoodoo
12
+
13
+ # The Communicators module is used as a namespace for
14
+ # Hoodoo::Communicators::Pool and its related utility classes,
15
+ # Hoodoo::Communicators::Fast and Hoodoo::Communicators::Fast.
16
+ #
17
+ module Communicators
18
+ end
19
+ end
20
+
21
+ require 'hoodoo/communicators/pool'
22
+ require 'hoodoo/communicators/fast'
23
+ require 'hoodoo/communicators/slow'
@@ -0,0 +1,44 @@
1
+ ########################################################################
2
+ # File:: fast.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: A fast communication-orientated object intended to be called
6
+ # synchronously via Hoodoo::Communicators::Pool.
7
+ # ----------------------------------------------------------------------
8
+ # 15-Dec-2014 (ADH): Created.
9
+ ########################################################################
10
+
11
+ module Hoodoo
12
+ module Communicators
13
+
14
+ # See Hoodoo::Communicators::Pool for details.
15
+ #
16
+ # A "fast communicator". Subclass this to create a class where instances
17
+ # are invoked via Hoodoo::Communicators::Fast#communicate with some
18
+ # parameter and, in response, they talk to some other piece of software to
19
+ # communicate information related to that parameter. The communication is
20
+ # expected to be fast and will be called from Ruby's main execution thread.
21
+ #
22
+ # If the communicator takes a long time to complete its operation, other
23
+ # processing will be delayed. If you expect this to happen, subclass
24
+ # Hoodoo::Communicators::Slow instead.
25
+ #
26
+ # Example: A communicator might be part of a logging scheme which writes
27
+ # to +STDOUT+. The parameter it expects would be a log message string.
28
+ #
29
+ class Fast
30
+
31
+ # Communicate quickly with the piece of external software for which your
32
+ # subclass is designed. Subclasses _must_ implement this method. There is
33
+ # no need to call +super+ in your implementation.
34
+ #
35
+ # +object+:: Parameter sent by the communication pool, in response to
36
+ # someone calling Hoodoo::Communicators::Pool#communicate
37
+ # with that value.
38
+ #
39
+ def communicate( object )
40
+ raise( 'Subclasses must implement #communicate' )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,601 @@
1
+ ########################################################################
2
+ # File:: pool.rb
3
+ # (C):: Loyalty New Zealand 2014
4
+ #
5
+ # Purpose:: A pool of communication-orientated objects which are either
6
+ # fast and operate synchronously, or are slow and are called
7
+ # asynchronously via a Ruby Thread.
8
+ # ----------------------------------------------------------------------
9
+ # 15-Dec-2014 (ADH): Created.
10
+ ########################################################################
11
+
12
+ module Hoodoo
13
+ module Communicators
14
+
15
+ # Maintains a pool of object instances which are expected to be
16
+ # communicating with "the outside world" in some way. A message
17
+ # sent to the pool is replicated to all the communicators in
18
+ # that pool. Some communicators are fast, which means they are
19
+ # called synchronously and expected to return very quickly. Some
20
+ # communicators are slow, which means they are called
21
+ # asynchronously through a work queue.
22
+ #
23
+ # See #add for more information.
24
+ #
25
+ class Pool
26
+
27
+ # Hoodoo::Communicators::Slow subclass communicators are called in
28
+ # their own Threads via a processing Queue. There is the potential for
29
+ # a flood of communications to cause the queue to back up considerably,
30
+ # so a maximum number of messages is defined. If the queue size is
31
+ # _equal to or greater_ than this amount when a message arrives, it
32
+ # will be dropped and a 'dropped message' count incremented.
33
+ #
34
+ MAX_SLOW_QUEUE_SIZE = 50
35
+
36
+ # When asking slow communicator threads to exit, a timeout must be used
37
+ # in case the thread doesn't seem to be responsive. This is the timeout
38
+ # value in seconds - it can take a floating point or integer value.
39
+ #
40
+ THREAD_EXIT_TIMEOUT = 5
41
+
42
+ # Analogous to THREAD_WAIT_TIMEOUT but used when waiting for a
43
+ # processing Thread to drain its Queue, without asking it to exit.
44
+ #
45
+ THREAD_WAIT_TIMEOUT = 5
46
+
47
+ # Retrieve the ThreadGroup instance managing the collection of slow
48
+ # communicator threads. This is mostly used for testing purposes and
49
+ # has little general purpose utility.
50
+ #
51
+ attr_accessor :group
52
+
53
+ # Create a new pool of communicators - instances of subclasses of
54
+ # Hoodoo::Communicators::Fast or Hoodoo::Communicators::Slow,
55
+ # are added with #add and called with #communicate.
56
+ #
57
+ def initialize
58
+ @pool = {}
59
+ @group = ::ThreadGroup.new
60
+ end
61
+
62
+ # Add a communicator instance to the pool. Future calls to #communicate
63
+ # will call the same-named method in that instance.
64
+ #
65
+ # Subclasses of Hoodoo::Communicators::Slow are called within a
66
+ # processing Thread. Subclasses of Hoodoo::Communicators::Fast are
67
+ # called inline. The instances are called in the order of addition, but
68
+ # since each slow communicator runs in its own Thread, the execution
69
+ # order is indeterminate for such instances.
70
+ #
71
+ # If a slow communicator's inbound message queue length matches or
72
+ # exceeds MAX_SLOW_QUEUE_SIZE, messages for that specific communicator
73
+ # will start being dropped until the communicator clears the backlog and
74
+ # at last one space opens on the queue. Slow communicators can detect
75
+ # when this has happened by implementing
76
+ # Hoodoo::Communicators::Slow#dropped in the subclass.
77
+ #
78
+ # If you pass the same instance more than once, the subsequent calls are
79
+ # ignored. You can add many instances of the same class if that's useful
80
+ # for any reason.
81
+ #
82
+ # Returns the passed-in communicator instance parameter, for convenience.
83
+ #
84
+ # +communicator+:: Instance is to be added to the pool. Must be
85
+ # either a Hoodoo::Communicators::Fast or
86
+ # Hoodoo::Communicators::Slow subclass instance.
87
+ #
88
+ def add( communicator )
89
+ unless ( communicator.class < Hoodoo::Communicators::Fast ||
90
+ communicator.class < Hoodoo::Communicators::Slow )
91
+ raise "Hoodoo::Communicators::Pool\#add must be called with an instance of a subclass of Hoodoo::Communicators::Fast or Hoodoo::Communicators::Slow only"
92
+ end
93
+
94
+ return if @pool.has_key?( communicator )
95
+
96
+ if communicator.is_a?( Hoodoo::Communicators::Fast )
97
+ add_fast_communicator( communicator )
98
+ else
99
+ add_slow_communicator( communicator )
100
+ end
101
+
102
+ return communicator
103
+ end
104
+
105
+ # Remove a communicator previously added by #add. See that for details.
106
+ #
107
+ # It is harmless to try and remove communicator instances more than once
108
+ # or to try to remove something that was never added in the first place;
109
+ # the call simply has no side effects.
110
+ #
111
+ # If removing a slow communicator, its thread will be terminated with
112
+ # default timeout value of THREAD_EXIT_TIMEOUT seconds. For this
113
+ # reason, removing a slow communicator may take a long time.
114
+ #
115
+ # Returns the passed-in communicator instance parameter, for convenience.
116
+ #
117
+ # +communicator+:: Instance is to be removed from the pool. Must be
118
+ # either a Hoodoo::Communicators::Fast or
119
+ # Hoodoo::Communicators::Slow subclass instance.
120
+ #
121
+ def remove( communicator )
122
+ unless ( communicator.class < Hoodoo::Communicators::Fast ||
123
+ communicator.class < Hoodoo::Communicators::Slow )
124
+ raise "Hoodoo::Communicators::Pool\#remove must be called with an instance of a subclass of Hoodoo::Communicators::Fast or Hoodoo::Communicators::Slow only"
125
+ end
126
+
127
+ return unless @pool.has_key?( communicator )
128
+
129
+ if communicator.is_a?( Hoodoo::Communicators::Fast )
130
+ remove_fast_communicator( communicator )
131
+ else
132
+ remove_slow_communicator( communicator )
133
+ end
134
+
135
+ return communicator
136
+ end
137
+
138
+ # Call the #communicate method on each communicator instance added via
139
+ # #add. Each instance is called in the same order as corresponding
140
+ # calls are made to the pool. _Across_ instances, fast communicators are
141
+ # called in the order they were added to the pool, but since each slow
142
+ # communicator runs in its own Thread, execution order is indeterminate.
143
+ #
144
+ # +object+:: Parameter passed to the communicator subclass instance
145
+ # #communicate methods.
146
+ #
147
+ def communicate( object )
148
+ @pool.each do | communicator, item |
149
+
150
+ if item.has_key?( :fast )
151
+ begin
152
+ communicator.communicate( object )
153
+ rescue => exception
154
+ handle_exception( exception, communicator )
155
+ end
156
+
157
+ else
158
+ data = item[ :slow ]
159
+ thread = data[ :thread ]
160
+ work_queue = data[ :work_queue ]
161
+
162
+ # This is inaccurate if one or more "dropped messages" reports are
163
+ # on the queue, but since some communicators might report them in
164
+ # the same way as other messages, it's not necessarily incorrect
165
+ # either.
166
+ #
167
+ if work_queue.size < MAX_SLOW_QUEUE_SIZE
168
+ dropped = thread[ :dropped_messages ]
169
+
170
+ if dropped > 0
171
+ thread[ :dropped_messages ] = 0
172
+
173
+ # Opposite of comment above on MAX_SLOW_QUEUE_SIZE check...
174
+ # Yes, this takes up a queue entry and the payload addition
175
+ # afterwards might take it one above max size, but that's OK
176
+ # since this is just a "dropped messages" report and though
177
+ # some communicators might deal with them slowly, others may
178
+ # just ignore them.
179
+ #
180
+ work_queue << QueueEntry.new( dropped: dropped )
181
+ end
182
+
183
+ work_queue << QueueEntry.new( payload: object )
184
+
185
+ else
186
+ thread[ :dropped_messages ] += 1
187
+
188
+ end
189
+ end
190
+
191
+ end
192
+ end
193
+
194
+ # This method is only useful if there are any
195
+ # Hoodoo::Communicators::Slow subclass instances in the communication
196
+ # pool. Each instance is called via a worker Thread; this method waits
197
+ # for each communicator to drain its queue before returning. This is
198
+ # useful if you have a requirement to wait for all communications to
199
+ # finish on all threads, presumably for wider synchronisation reasons.
200
+ #
201
+ # Since fast communicators are called synchronously there is never any
202
+ # need to wait for them, so this call ignores such pool entries.
203
+ #
204
+ # The following *named* parameters are supported:
205
+ #
206
+ # +per_instance_timeout+:: Timeout for _each_ slow communicator Thread
207
+ # in seconds. Optional. Default is the value
208
+ # in THREAD_WAIT_TIMEOUT.
209
+ #
210
+ # +communicator+:: If you want to wait for specific instance only
211
+ # (see #add), pass it here. If the instance is a
212
+ # fast communicator, or any object not added to
213
+ # the pool, then there is no error raised. The
214
+ # method simply returns immediately.
215
+ #
216
+ def wait( per_instance_timeout: THREAD_WAIT_TIMEOUT,
217
+ communicator: nil )
218
+
219
+ if communicator.nil?
220
+ @pool.each do | communicator, item |
221
+ next unless item.has_key?( :slow )
222
+ data = item[ :slow ]
223
+
224
+ wait_for(
225
+ work_queue: data[ :work_queue ],
226
+ sync_queue: data[ :sync_queue ],
227
+ timeout: per_instance_timeout
228
+ )
229
+ end
230
+
231
+ else
232
+ return unless @pool.has_key?( communicator )
233
+ item = @pool[ communicator ]
234
+
235
+ return unless item.has_key?( :slow )
236
+ data = item[ :slow ]
237
+
238
+ wait_for(
239
+ work_queue: data[ :work_queue ],
240
+ sync_queue: data[ :sync_queue ],
241
+ timeout: per_instance_timeout
242
+ )
243
+
244
+ end
245
+ end
246
+
247
+ # The communication pool is "emptied" by this call, going back to a
248
+ # clean state as if just initialised. New workers can be added via #add
249
+ # and then called via #communicate if you so wish.
250
+ #
251
+ # Hoodoo::Communciators::Fast subclass instances are removed
252
+ # immediately without complications.
253
+ #
254
+ # Hoodoo::Communicators::Slow subclass instances in the communication
255
+ # pool are called via a worker Thread; this method shuts down all such
256
+ # worker Threads, clearing their work queues and asking each one to exit
257
+ # (politely). There is no mechanism (other than overall Ruby process
258
+ # exit) available to shut down the Threads by force, so some Threads may
259
+ # not respond and time out.
260
+ #
261
+ # When this method exits, all workers will have either exited or timed
262
+ # out and possibly still be running, but are considered too slow or dead.
263
+ # No further communications are made to them.
264
+ #
265
+ # The following *named* parameters are supported:
266
+ #
267
+ # +per_instance_timeout+:: Timeout for _each_ slow communicator Thread
268
+ # in seconds. Optional. Default is the value
269
+ # in THREAD_EXIT_TIMEOUT. For example,
270
+ # with three slow communicators in the pool
271
+ # and all three reached a 5 second timeout,
272
+ # the termination method would not return for
273
+ # 15 seconds (3 * 5 seconds full timeout).
274
+ #
275
+ def terminate( per_instance_timeout: THREAD_EXIT_TIMEOUT )
276
+ loop do
277
+ klass, item = @pool.shift() # Hash#shift -> remove a key/value pair.
278
+ break if klass.nil?
279
+
280
+ next unless item.has_key?( :slow )
281
+ data = item[ :slow ]
282
+
283
+ request_termination_for(
284
+ thread: data[ :thread ],
285
+ work_queue: data[ :work_queue ],
286
+ timeout: per_instance_timeout
287
+ )
288
+ end
289
+ end
290
+
291
+ private
292
+
293
+ # Add a fast communicator to the pool. Requires no thread or queue.
294
+ #
295
+ # Trusted internal interface - pass the correct subclass and don't pass
296
+ # it more than once unless #terminate has cleared the pool beforehand.
297
+ #
298
+ # +communicator+:: The Hoodoo::Communicators::Fast subclass instance
299
+ # to add to the pool.
300
+ #
301
+ def add_fast_communicator( communicator )
302
+ @pool[ communicator ] = { :fast => true }
303
+ end
304
+
305
+ # Remove a fast communicator from the pool. See #add_fast_communicator.
306
+ #
307
+ # +communicator+:: The Hoodoo::Communicators::Fast subclass instance
308
+ # to remove from the pool.
309
+ #
310
+ def remove_fast_communicator( communicator )
311
+ @pool.delete( communicator )
312
+ end
313
+
314
+ # Add a slow communicator to the pool. Requires a thread and queue.
315
+ #
316
+ # Trusted internal interface - pass the correct subclass and don't pass
317
+ # it more than once unless #terminate has cleared the pool beforehand.
318
+ #
319
+ # +communicator+:: The Hoodoo::Communicators::Slow subclass instance
320
+ # to add to the pool.
321
+ #
322
+ def add_slow_communicator( communicator )
323
+
324
+ work_queue = ::Queue.new
325
+ sync_queue = QueueWithTimeout.new
326
+
327
+ # Start (and keep a reference to) a thread that just loops around
328
+ # processing queue messages until asked to exit.
329
+
330
+ thread = ::Thread.new do
331
+
332
+ # Outer infinite loop restarts queue processing if exceptions occur.
333
+ #
334
+ loop do
335
+
336
+ # Exception handler block.
337
+ #
338
+ begin
339
+
340
+ # Inner infinite loop processes queue objects until asked to exit
341
+ # via a +nil+ queue entry.
342
+ #
343
+ loop do
344
+ entry = work_queue.shift() # ".shift" => FIFO, ".pop" would be LIFO
345
+
346
+ if entry.terminate?
347
+ ::Thread.exit
348
+ elsif entry.sync?
349
+ sync_queue << :sync
350
+ elsif entry.dropped?
351
+ communicator.dropped( entry.dropped )
352
+ else
353
+ communicator.communicate( entry.payload )
354
+ end
355
+ end
356
+
357
+ rescue => exception
358
+ handle_exception( exception, communicator )
359
+
360
+ end
361
+ end
362
+ end
363
+
364
+ thread[ :dropped_messages ] = 0
365
+
366
+ @group.add( thread )
367
+ @pool[ communicator ] = {
368
+ :slow => {
369
+ :thread => thread,
370
+ :work_queue => work_queue,
371
+ :sync_queue => sync_queue,
372
+ }
373
+ }
374
+ end
375
+
376
+ # Remove a slow communicator from the pool. See #add_slow_communicator.
377
+ #
378
+ # May take a while to return, as it must request thread shutdown via
379
+ # #request_termination_for (and uses the default timeout for that).
380
+ #
381
+ # +communicator+:: The Hoodoo::Communicators::Slow subclass instance
382
+ # to remove from the pool.
383
+ #
384
+ def remove_slow_communicator( communicator )
385
+ item = @pool[ communicator ]
386
+ data = item[ :slow ]
387
+
388
+ request_termination_for(
389
+ thread: data[ :thread ],
390
+ work_queue: data[ :work_queue ]
391
+ )
392
+
393
+ @pool.delete( communicator )
394
+ end
395
+
396
+ # Ask a slow communicator Thread to exit. Existing work on any Queues
397
+ # is cleared first, so only the current in-process message for a given
398
+ # communicator has to finish prior to exit.
399
+ #
400
+ # *Named* parameters are:
401
+ #
402
+ # +:thread+:: Mandatory. Worker Thread for the communicator.
403
+ # +:work_queue+:: Mandatory. Queue used to send work to the Thread.
404
+ # +timeout+:: Optional timeout in seconds - default is
405
+ # THREAD_EXIT_TIMEOUT.
406
+ #
407
+ # The method returns if the timeout threshold is exceeded, without
408
+ # raising any exceptions.
409
+ #
410
+ def request_termination_for( thread:, work_queue:, timeout: THREAD_EXIT_TIMEOUT )
411
+ work_queue.clear()
412
+ work_queue << QueueEntry.new( terminate: true )
413
+
414
+ thread.join( timeout )
415
+ end
416
+
417
+ # Wait for a slow communicator Thread to empty its work Queue. *Named*
418
+ # parameters are:
419
+ #
420
+ # +:work_queue+:: Mandatory. Queue used to send work to the Thread.
421
+ # +:sync_queue+:: Mandatory. Queue used by that Thread to send back a
422
+ # sync notification to the pool.
423
+ # +timeout+:: Optional timeout in seconds - default is
424
+ # THREAD_WAIT_TIMEOUT.
425
+ #
426
+ # The method returns if the timeout threshold is exceeded, without
427
+ # raising any exceptions.
428
+ #
429
+ def wait_for( work_queue:, sync_queue:, timeout: THREAD_WAIT_TIMEOUT )
430
+
431
+ # Push a 'sync' entry onto the work Queue. Once the worker Thread gets
432
+ # through other Queue items and reaches this entry, it'll respond
433
+ # by pushing an item onto its sync Queue.
434
+
435
+ work_queue << QueueEntry.new( sync: true )
436
+
437
+ # Wait on the sync Queue for the worker Thread to send the requested
438
+ # message indicating that we're in sync.
439
+
440
+ begin
441
+ sync_queue.shift( timeout )
442
+
443
+ rescue ThreadError
444
+ # Do nothing
445
+
446
+ end
447
+ end
448
+
449
+ # Intended for cases where a communicator raised an exception - print
450
+ # details to $stderr. This is all we can do; the logging engine runs
451
+ # through the communications pool so attempting to log an exception
452
+ # might cause an exception that we then attempt to log - and so-on.
453
+ #
454
+ # +exception+:: Exception (or Exception subclass) instance to print.
455
+ # +communicator+:: Communicator instance that raised the exception.
456
+ #
457
+ def handle_exception( exception, communicator )
458
+ begin
459
+ report = "Slow communicator class #{ communicator.class.name } raised exception '#{ exception }': #{ exception.backtrace }"
460
+ $stderr.puts( report )
461
+
462
+ rescue
463
+ # If the above fails then everything else is probably about to
464
+ # collapse, but optimistically try to ignore the error and keep
465
+ # the wider processing code alive.
466
+
467
+ end
468
+ end
469
+
470
+ # Internal implementation detail of Hoodoo::Communicators::Pool.
471
+ #
472
+ # Since pool clients can say "wait until (one or all) workers have
473
+ # processed their Queue contents", we need to have some way of seeing
474
+ # when all work is done. The clean way to do it is to push 'sync now'
475
+ # messages onto the communicator Threads work Queues, so that as they
476
+ # work through the Queue they'll eventually reach that message. They then
477
+ # push a message onto a sync Queue for that worker. Meanwhile the waiting
478
+ # pool does (e.g.) a +pop+ on the sync Queue, which means it blocks until
479
+ # the workers say they've finished. No busy waiting, Ruby gets to make
480
+ # its best guess at scheduling, etc.; all good.
481
+ #
482
+ # The catch? You can't use +Timeout::timeout...do...+ around a Queue
483
+ # +pop+. It just doesn't work. It's a strange omission and requires code
484
+ # gymnastics to work around.
485
+ #
486
+ # Enter QueueWithTimeout, from:
487
+ #
488
+ # http://spin.atomicobject.com/2014/07/07/ruby-queue-pop-timeout/
489
+ #
490
+ class QueueWithTimeout
491
+
492
+ # Create a new instance.
493
+ #
494
+ def initialize
495
+ @mutex = ::Mutex.new
496
+ @queue = []
497
+ @recieved = ::ConditionVariable.new
498
+ end
499
+
500
+ # Push a new entry to the end of the queue.
501
+ #
502
+ # +entry+:: Entry to put onto the end of the queue.
503
+ #
504
+ def <<( entry )
505
+ @mutex.synchronize do
506
+ @queue << entry
507
+ @recieved.signal
508
+ end
509
+ end
510
+
511
+ # Take an entry from the front of the queue (FIFO) with optional
512
+ # timeout if the queue is empty.
513
+ #
514
+ # +timeout+:: Timeout (in seconds, Integer or Float) to wait for an
515
+ # item to appear on the queue, if the queue is empty. If
516
+ # +nil+, there is no timeout (waits indefinitely).
517
+ # Optional; default is +nil+.
518
+ #
519
+ # If given a non-+nil+ timeout value and the timeout expires, raises
520
+ # a ThreadError exception (just as non-blocking Ruby Queue#pop would).
521
+ #
522
+ def shift( timeout = nil )
523
+ @mutex.synchronize do
524
+ if @queue.empty?
525
+ @recieved.wait( @mutex, timeout ) if timeout != 0
526
+ raise( ThreadError, 'queue empty' ) if @queue.empty?
527
+ end
528
+
529
+ @queue.shift
530
+ end
531
+ end
532
+ end
533
+
534
+ # Internal implementation detail of Hoodoo::Communicators::Pool which
535
+ # is placed on a Ruby Queue and used as part of thread processing for
536
+ # slow communicators.
537
+ #
538
+ class QueueEntry
539
+
540
+ # If +true+, the processing Thread should exit. See also #terminate?.
541
+ #
542
+ attr_accessor :terminate
543
+
544
+ # If +true+, the processing Thread should push one item with any
545
+ # payload onto its sync Queue. See also #sync?
546
+ #
547
+ attr_accessor :sync
548
+
549
+ # If not +nil+ or zero, the number of dropped messages that should be
550
+ # send to the slow communicator subclass's #dropped method. See also
551
+ # #dropped?
552
+ #
553
+ attr_accessor :dropped
554
+
555
+ # If the entry represents neither a termination request nor a dropped
556
+ # message count (see #terminate? and #dropped?), the payload to send to
557
+ # the slow communicator subclass's #communicate method.
558
+ #
559
+ attr_accessor :payload
560
+
561
+ # Create a new instance, ready to be added to the Queue.
562
+ #
563
+ # *ONLY* *USE* *ONE* of the named parameters:
564
+ #
565
+ # +payload+:: A parameter to send to #communicate in the communicator.
566
+ # +dropped+:: The integer to send to #dropped in the communicator.
567
+ # +terminate+:: Set to +true+ to exit the processing thread when the
568
+ # entry is read from the Queue.
569
+ # +sync+:: Set to +true+ to push a message onto the sync Queue.
570
+ #
571
+ def initialize( payload: nil, dropped: nil, terminate: false, sync: false )
572
+ @payload = payload
573
+ @dropped = dropped
574
+ @terminate = terminate
575
+ @sync = sync
576
+ end
577
+
578
+ # Returns +true+ if encountering this queue entry should terminate the
579
+ # processing thread, else +false+ (see #dropped? then #payload).
580
+ #
581
+ def terminate?
582
+ @terminate == true
583
+ end
584
+
585
+ # Returns +true+ if this queue entry represents a request to push a
586
+ # message onto the processing Thread's sync Queue.
587
+ #
588
+ def sync?
589
+ @sync == true
590
+ end
591
+
592
+ # Returns +true+ if this queue entry represents a dropped message count
593
+ # (see #dropped), else +false (see #terminate? then #payload).
594
+ #
595
+ def dropped?
596
+ @dropped != nil && @dropped > 0
597
+ end
598
+ end
599
+ end
600
+ end
601
+ end