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.
- checksums.yaml +7 -0
- data/bin/hoodoo +5 -0
- data/lib/hoodoo.rb +27 -0
- data/lib/hoodoo/active.rb +32 -0
- data/lib/hoodoo/active/active_model/uuid_validator.rb +45 -0
- data/lib/hoodoo/active/active_record/base.rb +81 -0
- data/lib/hoodoo/active/active_record/creator.rb +134 -0
- data/lib/hoodoo/active/active_record/dated.rb +343 -0
- data/lib/hoodoo/active/active_record/error_mapping.rb +351 -0
- data/lib/hoodoo/active/active_record/finder.rb +606 -0
- data/lib/hoodoo/active/active_record/search_helper.rb +189 -0
- data/lib/hoodoo/active/active_record/secure.rb +431 -0
- data/lib/hoodoo/active/active_record/support.rb +106 -0
- data/lib/hoodoo/active/active_record/translated.rb +87 -0
- data/lib/hoodoo/active/active_record/uuid.rb +80 -0
- data/lib/hoodoo/active/active_record/writer.rb +321 -0
- data/lib/hoodoo/client.rb +23 -0
- data/lib/hoodoo/client/augmented_array.rb +29 -0
- data/lib/hoodoo/client/augmented_base.rb +168 -0
- data/lib/hoodoo/client/augmented_hash.rb +23 -0
- data/lib/hoodoo/client/client.rb +354 -0
- data/lib/hoodoo/client/endpoint/endpoint.rb +427 -0
- data/lib/hoodoo/client/endpoint/endpoints/amqp.rb +180 -0
- data/lib/hoodoo/client/endpoint/endpoints/auto_session.rb +194 -0
- data/lib/hoodoo/client/endpoint/endpoints/http.rb +203 -0
- data/lib/hoodoo/client/endpoint/endpoints/http_based.rb +367 -0
- data/lib/hoodoo/client/endpoint/endpoints/not_found.rb +59 -0
- data/lib/hoodoo/client/headers.rb +269 -0
- data/lib/hoodoo/communicators.rb +23 -0
- data/lib/hoodoo/communicators/fast.rb +44 -0
- data/lib/hoodoo/communicators/pool.rb +601 -0
- data/lib/hoodoo/communicators/slow.rb +84 -0
- data/lib/hoodoo/data.rb +51 -0
- data/lib/hoodoo/data/resources/caller.rb +39 -0
- data/lib/hoodoo/data/resources/errors.rb +28 -0
- data/lib/hoodoo/data/resources/log.rb +31 -0
- data/lib/hoodoo/data/resources/session.rb +26 -0
- data/lib/hoodoo/data/types/error_primitive.rb +27 -0
- data/lib/hoodoo/data/types/permissions.rb +40 -0
- data/lib/hoodoo/data/types/permissions_defaults.rb +32 -0
- data/lib/hoodoo/data/types/permissions_full.rb +28 -0
- data/lib/hoodoo/data/types/permissions_resources.rb +31 -0
- data/lib/hoodoo/discovery.rb +20 -0
- data/lib/hoodoo/errors.rb +19 -0
- data/lib/hoodoo/errors/error_descriptions.rb +229 -0
- data/lib/hoodoo/errors/errors.rb +322 -0
- data/lib/hoodoo/generator.rb +139 -0
- data/lib/hoodoo/logger.rb +23 -0
- data/lib/hoodoo/logger/fast_writer.rb +27 -0
- data/lib/hoodoo/logger/flattener_mixin.rb +36 -0
- data/lib/hoodoo/logger/logger.rb +387 -0
- data/lib/hoodoo/logger/slow_writer.rb +49 -0
- data/lib/hoodoo/logger/writer_mixin.rb +52 -0
- data/lib/hoodoo/logger/writers/file_writer.rb +45 -0
- data/lib/hoodoo/logger/writers/log_entries_dot_com_writer.rb +64 -0
- data/lib/hoodoo/logger/writers/stream_writer.rb +43 -0
- data/lib/hoodoo/middleware.rb +33 -0
- data/lib/hoodoo/presenters.rb +45 -0
- data/lib/hoodoo/presenters/base.rb +281 -0
- data/lib/hoodoo/presenters/base_dsl.rb +519 -0
- data/lib/hoodoo/presenters/common_resource_fields.rb +31 -0
- data/lib/hoodoo/presenters/embedding.rb +232 -0
- data/lib/hoodoo/presenters/types/array.rb +118 -0
- data/lib/hoodoo/presenters/types/boolean.rb +26 -0
- data/lib/hoodoo/presenters/types/date.rb +26 -0
- data/lib/hoodoo/presenters/types/date_time.rb +26 -0
- data/lib/hoodoo/presenters/types/decimal.rb +47 -0
- data/lib/hoodoo/presenters/types/enum.rb +55 -0
- data/lib/hoodoo/presenters/types/field.rb +158 -0
- data/lib/hoodoo/presenters/types/float.rb +26 -0
- data/lib/hoodoo/presenters/types/hash.rb +361 -0
- data/lib/hoodoo/presenters/types/integer.rb +26 -0
- data/lib/hoodoo/presenters/types/object.rb +117 -0
- data/lib/hoodoo/presenters/types/string.rb +53 -0
- data/lib/hoodoo/presenters/types/tags.rb +24 -0
- data/lib/hoodoo/presenters/types/text.rb +26 -0
- data/lib/hoodoo/presenters/types/uuid.rb +54 -0
- data/lib/hoodoo/services.rb +34 -0
- data/lib/hoodoo/services/discovery/discoverers/by_consul.rb +66 -0
- data/lib/hoodoo/services/discovery/discoverers/by_convention.rb +173 -0
- data/lib/hoodoo/services/discovery/discoverers/by_drb/by_drb.rb +195 -0
- data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server.rb +166 -0
- data/lib/hoodoo/services/discovery/discoverers/by_drb/drb_server_start.rb +37 -0
- data/lib/hoodoo/services/discovery/discovery.rb +186 -0
- data/lib/hoodoo/services/discovery/results/for_amqp.rb +58 -0
- data/lib/hoodoo/services/discovery/results/for_http.rb +85 -0
- data/lib/hoodoo/services/discovery/results/for_local.rb +85 -0
- data/lib/hoodoo/services/discovery/results/for_remote.rb +57 -0
- data/lib/hoodoo/services/middleware/amqp_log_message.rb +186 -0
- data/lib/hoodoo/services/middleware/amqp_log_writer.rb +119 -0
- data/lib/hoodoo/services/middleware/endpoints/inter_resource_local.rb +130 -0
- data/lib/hoodoo/services/middleware/endpoints/inter_resource_remote.rb +202 -0
- data/lib/hoodoo/services/middleware/exception_reporting/base_reporter.rb +105 -0
- data/lib/hoodoo/services/middleware/exception_reporting/exception_reporting.rb +115 -0
- data/lib/hoodoo/services/middleware/exception_reporting/reporters/airbrake_reporter.rb +64 -0
- data/lib/hoodoo/services/middleware/exception_reporting/reporters/raygun_reporter.rb +63 -0
- data/lib/hoodoo/services/middleware/interaction.rb +127 -0
- data/lib/hoodoo/services/middleware/middleware.rb +2705 -0
- data/lib/hoodoo/services/middleware/rack_monkey_patch.rb +73 -0
- data/lib/hoodoo/services/services/context.rb +153 -0
- data/lib/hoodoo/services/services/implementation.rb +132 -0
- data/lib/hoodoo/services/services/interface.rb +934 -0
- data/lib/hoodoo/services/services/permissions.rb +250 -0
- data/lib/hoodoo/services/services/request.rb +189 -0
- data/lib/hoodoo/services/services/response.rb +316 -0
- data/lib/hoodoo/services/services/service.rb +141 -0
- data/lib/hoodoo/services/services/session.rb +729 -0
- data/lib/hoodoo/utilities.rb +12 -0
- data/lib/hoodoo/utilities/string_inquirer.rb +54 -0
- data/lib/hoodoo/utilities/utilities.rb +380 -0
- data/lib/hoodoo/utilities/uuid.rb +44 -0
- data/lib/hoodoo/version.rb +17 -0
- data/spec/active/active_record/base_spec.rb +57 -0
- data/spec/active/active_record/creator_spec.rb +88 -0
- data/spec/active/active_record/dated_spec.rb +248 -0
- data/spec/active/active_record/error_mapping_spec.rb +360 -0
- data/spec/active/active_record/finder_spec.rb +744 -0
- data/spec/active/active_record/search_helper_spec.rb +384 -0
- data/spec/active/active_record/secure_spec.rb +435 -0
- data/spec/active/active_record/support_spec.rb +225 -0
- data/spec/active/active_record/translated_spec.rb +19 -0
- data/spec/active/active_record/uuid_spec.rb +72 -0
- data/spec/active/active_record/writer_spec.rb +272 -0
- data/spec/alchemy/alchemy-amq.rb +33 -0
- data/spec/client/augmented_array_spec.rb +15 -0
- data/spec/client/augmented_base_spec.rb +50 -0
- data/spec/client/augmented_hash_spec.rb +15 -0
- data/spec/client/client_spec.rb +955 -0
- data/spec/client/endpoint/endpoint_spec.rb +70 -0
- data/spec/client/endpoint/endpoints/amqp_spec.rb +16 -0
- data/spec/client/endpoint/endpoints/auto_session_spec.rb +9 -0
- data/spec/client/endpoint/endpoints/http_based_spec.rb +9 -0
- data/spec/client/endpoint/endpoints/http_spec.rb +103 -0
- data/spec/client/endpoint/endpoints/not_found_spec.rb +35 -0
- data/spec/client/headers_spec.rb +172 -0
- data/spec/communicators/fast_spec.rb +9 -0
- data/spec/communicators/pool_spec.rb +339 -0
- data/spec/communicators/slow_spec.rb +15 -0
- data/spec/data/resources/caller_spec.rb +156 -0
- data/spec/data/resources/errors_spec.rb +22 -0
- data/spec/data/resources/log_spec.rb +20 -0
- data/spec/data/resources/session_spec.rb +15 -0
- data/spec/data/types/error_primitive_spec.rb +15 -0
- data/spec/data/types/permissions_defaults_spec.rb +25 -0
- data/spec/data/types/permissions_full_spec.rb +44 -0
- data/spec/data/types/permissions_resources_spec.rb +34 -0
- data/spec/data/types/permissions_spec.rb +37 -0
- data/spec/errors/error_descriptions_spec.rb +98 -0
- data/spec/errors/errors_spec.rb +346 -0
- data/spec/integration/service_actions_spec.rb +112 -0
- data/spec/logger/fast_writer_spec.rb +18 -0
- data/spec/logger/logger_spec.rb +259 -0
- data/spec/logger/slow_writer_spec.rb +144 -0
- data/spec/logger/writers/file_writer_spec.rb +37 -0
- data/spec/logger/writers/log_entries_dot_com_writer_spec.rb +29 -0
- data/spec/logger/writers/stream_writer_spec.rb +38 -0
- data/spec/presenters/base_dsl_spec.rb +111 -0
- data/spec/presenters/base_spec.rb +871 -0
- data/spec/presenters/common_resource_fields_spec.rb +30 -0
- data/spec/presenters/embedding_spec.rb +87 -0
- data/spec/presenters/types/array_spec.rb +249 -0
- data/spec/presenters/types/boolean_spec.rb +51 -0
- data/spec/presenters/types/date_spec.rb +57 -0
- data/spec/presenters/types/date_time_spec.rb +59 -0
- data/spec/presenters/types/decimal_spec.rb +58 -0
- data/spec/presenters/types/enum_spec.rb +71 -0
- data/spec/presenters/types/field_spec.rb +77 -0
- data/spec/presenters/types/float_spec.rb +50 -0
- data/spec/presenters/types/hash_spec.rb +1069 -0
- data/spec/presenters/types/integer_spec.rb +50 -0
- data/spec/presenters/types/object_spec.rb +177 -0
- data/spec/presenters/types/string_spec.rb +65 -0
- data/spec/presenters/types/tags_spec.rb +56 -0
- data/spec/presenters/types/text_spec.rb +50 -0
- data/spec/presenters/types/uuid_spec.rb +46 -0
- data/spec/presenters/walk_spec.rb +198 -0
- data/spec/services/discovery/discoverers/by_consul_spec.rb +29 -0
- data/spec/services/discovery/discoverers/by_convention_spec.rb +67 -0
- data/spec/services/discovery/discoverers/by_drb/by_drb_spec.rb +80 -0
- data/spec/services/discovery/discoverers/by_drb/drb_server_spec.rb +205 -0
- data/spec/services/discovery/discovery_spec.rb +73 -0
- data/spec/services/discovery/results/for_amqp_spec.rb +17 -0
- data/spec/services/discovery/results/for_http_spec.rb +37 -0
- data/spec/services/discovery/results/for_local_spec.rb +21 -0
- data/spec/services/discovery/results/for_remote_spec.rb +15 -0
- data/spec/services/middleware/amqp_log_message_spec.rb +60 -0
- data/spec/services/middleware/amqp_log_writer_spec.rb +95 -0
- data/spec/services/middleware/endpoints/inter_resource_local_spec.rb +9 -0
- data/spec/services/middleware/endpoints/inter_resource_remote_spec.rb +9 -0
- data/spec/services/middleware/exception_reporting/base_reporter_spec.rb +16 -0
- data/spec/services/middleware/exception_reporting/exception_reporting_spec.rb +92 -0
- data/spec/services/middleware/exception_reporting/reporters/airbrake_reporter_spec.rb +24 -0
- data/spec/services/middleware/exception_reporting/reporters/raygun_reporter_spec.rb +23 -0
- data/spec/services/middleware/middleware_cors_spec.rb +93 -0
- data/spec/services/middleware/middleware_create_update_spec.rb +489 -0
- data/spec/services/middleware/middleware_dated_at_spec.rb +186 -0
- data/spec/services/middleware/middleware_exotic_communication_spec.rb +560 -0
- data/spec/services/middleware/middleware_logging_spec.rb +356 -0
- data/spec/services/middleware/middleware_multi_local_spec.rb +1094 -0
- data/spec/services/middleware/middleware_multi_remote_spec.rb +1440 -0
- data/spec/services/middleware/middleware_permissions_spec.rb +1014 -0
- data/spec/services/middleware/middleware_public_spec.rb +238 -0
- data/spec/services/middleware/middleware_spec.rb +1569 -0
- data/spec/services/middleware/string_inquirer_spec.rb +30 -0
- data/spec/services/services/application_spec.rb +74 -0
- data/spec/services/services/context_spec.rb +48 -0
- data/spec/services/services/implementation_spec.rb +45 -0
- data/spec/services/services/interface_spec.rb +262 -0
- data/spec/services/services/permissions_spec.rb +249 -0
- data/spec/services/services/request_spec.rb +95 -0
- data/spec/services/services/response_spec.rb +250 -0
- data/spec/services/services/session_spec.rb +432 -0
- data/spec/spec_helper.rb +298 -0
- data/spec/utilities/utilities_spec.rb +537 -0
- data/spec/utilities/uuid_spec.rb +20 -0
- 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
|