9. Message Type Modules¶
What’s A Message Type Module?¶
Message type modules give AMPS the ability to process the contents of messages. AMPS includes implementations of FIX, NVFIX, JSON and XML. To process other types of messages, you implement a message type module.
Message type modules implement four basic sets of capabilities. These sets of capabilities are:
- Topic based routing. These functions provide the basic infrastructure for loading the module. Since topic routing is managed without needing to parse the message, simply allowing a module to be initialized provides the ability to route messages by topic.
- Content filtering and SOW persistence. To provide content filtering, the AMPS module interface specifies a pair of functions that parse the content of the message into the data structures that AMPS uses for filtering. These functions also allow AMPS to find the SOW key in a message so that State of the World databases can function correctly.
- Aggregation, Joins, Views, and Delta Subscribe. The AMPS module
interface specifies a set of functions used to create messages from
value sets. These functions translate AMPS data structures back into
the message format. The functions allow AMPS to create messages of
the message type, which enables aggregation, joins and views, and
delta subscription. These functions can also enable AMPS status
messages (for example, messages to the
/AMPS/ClientStatus
topic) for the message type. - Delta publish. This function allows AMPS to accept delta publishes by merging a partial message into an existing message.
Each set of capabilities builds on the previous set. For example, to provide aggregation, a module must also provide content filtering and module initialization. The following table lists the functions for each level of capability.
To enable: | Implement: |
---|---|
Topic based routing |
|
Content filtering and SOW persistence |
|
Aggregation, JOIN, View, Delta Subscribe |
|
Delta Publish | amps_message_type_delta_publish_merge |
Formatting for Log Messages | amps_message_type_trace_message |
Table 9.1: Message Type Capabilities and Functions
When to Implement a Message Type Module¶
Implement a message type module when you need to provide content
filtering, aggregation, or delta publishing for an existing native
message format that AMPS does not currently support. If you need only
topic-based filtering, you can use the binary
message type. If you
can extract relevant information out of the message body into a
preamble, you can use the composite message types to provide filtering
(and so forth) over the preamble, and use binary
for the original
message. In many cases, a composite message type that uses this pattern
will outperform a custom message type, since AMPS only needs to parse
the preamble.
However, if you have a need to provide a message payload format that is currently in use by a larger system and is not currently supported by AMPS, and if your system requires filtering, views and aggregation, or delta messaging over the entire message rather than a subset of the message, creating a message type module can be a good option.
Message type modules are performance-critical components for AMPS: the speed with which your module parses and serializes messages will directly affect how quickly AMPS can process messages of that type. Pay careful attention to the performance of your module. Minimize the amount of work your message type module performs during parsing, avoid unnecessary allocation or data copies, and minimize parsing when possible.
Caution
Message type modules are performance-critical components of AMPS. The speed of your message type module will directly affect the overall speed and throughput of your AMPS instance when processing messages of that type.
Module Initialization & Topic Based Routing¶
To provide topic based routing, a module simply needs to provide the functions that allow AMPS to initialize the module. Because topic based routing does not require interpretation of the message body, there is no need to provide functions that parse the message body.
AMPS provides the ability to configure message types globally, when the module loads, or in each use of the message type. Because the same module may be used with different parameters on different transports, AMPS allows you to save the parameters in an message type context.
Your module implements the amps_message_type_create_context function
to create
a message type context. The function has this signature:
amps_message_type_context amps_message_type_create_context(const amps_module_options options);
The message_type_context
type is a pointer to data that your module
defines. The AMPS server does not process or interpret this data. The
server receives the pointer from your module when the context is
initialized, and then provides the pointer back to your module with
requests to the module.
If your module is unable to successfully create the context, you return
a NULL
context and use the amps_logger
provided to your module to
log information about the problem.
Notice that you cast the pointer to amps_message_type_context
before
you return it, and cast the pointer back to the type that your
application uses after AMPS returns the pointer to you.
AMPS passes configuration options to modules as a pointer to an array of
amps_option
structs. AMPS indicates the end of the array with an
option that has a NULL
key. Your module processes options until it
reaches an option with a NULL
key. If no options are specified in the
configuration file, the firstamps_option
in the array will have a
NULL
key.
The following snippet shows one way to process options:
for (; options->key != NULL; ++options) {
// process the key/value pair here
}
When AMPS exits, AMPS calls amps_authentication_destroy_context
with each
of the authentication contexts created. This gives your module an opportunity
to do any resource management, cleanup or logging necessary before AMPS shuts
down. The function has this signature:
int amps_message_type_destroy_context(amps_message_type_context context);
Declaring Supported Operations¶
By default, AMPS uses the functions that a message type provides to
determine the operations that the message type supports. For general
purpose message types, this works well. For some types, however, not all
of the operations may be appropriate. For example, a message type that
supports a specific set of tags may want to support serialization for
views, but not for /AMPS/SOWStats
messages. For cases like this,
AMPS allows a module to explicitly declare the supported operations.
When the function amps_message_type_get_supported_options
is
present, AMPS calls the function for each context after context
initialization returns. The function returns a string that contains a
comma-delimited list of options supported by the module for that
context. The options are as follows:
Option | Capability |
---|---|
sow_stats |
This context supports serializing
/AMPS/SOWStats messages |
delta |
This context supports serializing delta messages. |
view |
This context supports serializing messages for views. |
client_status |
This context supports serializing
/AMPS/ClientStatus messages |
stop_timer |
This context supports serializing
StopTimer messages. |
select |
This context supports serializing partial
messages when a client uses the select
option on a query or subscription. |
Table 9.2: Message Type Capabilities
AMPS does not free the memory returned from this function: generally, modules that use this capability return a pointer to a static string. When a module implements this method, AMPS assumes that the module does not support any option that is not returned from the function, regardless of whether the module contains the functions that would support it.
AMPS Identifiers¶
AMPS locates values in a message using a compact, XPath-based syntax, as described in the AMPS User’s Guide. While creating your message type module, you decide how to interpret values in that syntax. The AMPS server does not enforce any specific semantics for an identifier: your module decides how to interpret the identifier.
There are two guidelines for how your module interprets identifiers:
- Your module should interpret the identifiers in a consistent way, and
interpret the same identifier the same way every time. For example,
if the module interprets
addresses/0
as the first element of an array of addresses at the root level of the message, your module should interpret/addresses/1
as the second element. - If your module implements message creation, your module should
produce output that can be parsed consistently. For example, if your
module interprets the message
a=b
as containing an element of/a
with a value ofb
, your module should not produce a different format when asked to serialize an identifier of/a
with a value ofb
. For simple message types, this is straightforward. For complex hierarchies or complicated translations, we recommend writing test cases to ensure that your module is producing the expected output.
For example, consider a message type that uses a fixed schema, and knows in advance the length and location for a given identifier. In this case, there would be no need for the message type to actually traverse the message to provide values to AMPS.
AMPS Values and Types¶
AMPS includes a lightweight, efficient type system for values within messages. This type system helps AMPS correctly filter and aggregate messages.
If your module provides content filtering capabilities or any of the
capabilities that depend on content filtering, it’s important to set the
type of parsed values correctly. Otherwise, AMPS performs comparisons
based on the type you provide, which can produce unexpected results. For
example, a string comparison "020" < "19"
has a different result
than a numeric comparison 20 < 19
.
The AMPS type system has two basic types:
- String values. These values are represented
as a
char*
pointing to the first character of the string and a length. Notice that, in AMPS modules, strings are not NULL-terminated. This enables string values within AMPS to handle embedded NULLs, provided that the message type module handles these values correctly.
Numeric values. These values are represented as a
union
that contains the following C++ types:unsigned long
,long
,double
, orbool
.When parsing a message, the message type module is responsible for identifying the most appropriate type for the value and setting the value appropriately. When serializing a numeric value, a message type module determines the type of the saved value, retrieves the value, and formats the value for serialization based on the type.
For messages that you parse, your module determines the type of the
parsed value and sets the amps_message_value
accordingly. For
messages provided by AMPS, AMPS provides the value and the type.
Once a parsed value is returned to AMPS, the server handles type conversion as necessary. A module does not have to explicitly provide type conversion, and a message type module cannot set restrictions on type conversion within the AMPS expression engine.
Parsing, Content Filtering and SOW¶
AMPS represents the contents of a parsed
message as a collection of value_set_entry
structs. Each
value_set_entry
contains the value itself, type information about
the value, and an XPath-based identifier that represents where the value
occurs in the message. Your message type module is responsible for
taking the data in the message and parsing it into value_set_entry
objects.
AMPS provides your module with the unparsed message, a value set to hold the parsed values, and (optionally) a set of tags that indicate values that AMPS currently needs to find in the message.
The basic outline of AMPS parsing is as follows:
- Find a tag and value in the message.
- Create a
value_set_entry
and set the properties of the entry. - Add the
value_set_entry
to the value set and store the index returned. - Use the index returned to indicate the document hierarchy, if necessary.
- If a set of values to find has been provided, your module can optionally check to see whether all of the values needed have been parsed. If all of the needed values have been parsed, your module can choose to finish parsing.
Message Parsing Example¶
You decide how to map the message format into XPath in way that is convenient and intuitive for your users. In general,a straightforward mapping is easiest to code and easiest for users to understand.
For example, consider the following simple message format:
messageId=5, customerId=20, notes=Have a nice day.
This message format uses simple, non-hierarchical tag=value pairs. Each
pair is separated by a comma. For this message, our message type module
will produce three value_set_entry
structs that contain the
following identifiers and values:
Tag | Value |
---|---|
messageId | 5 |
customerId | 20 |
notes | Have a nice day. |
Table 9.3: Flat Format Message Identifiers and Values
A similar approach works for more complicated formats:
{
"messageId" : 5,
"customerInfo" : {
"customerId" : 20,
"notes" : "Have a nice day."
}
}
Tag | Parent | Value |
---|---|---|
messageId | 5 | |
customerInfo | { “customerId” : 20, “notes” : “Have a nice day.” } | |
customerId | customerInfo | 20 |
notes | customerInfo | Have a nice day. |
Table 9.4: Hierarchical Format Example Identifiers and Values
The mapping is similar to the flat message approach. In particular,
notice that AMPS does not require a particular parsed value for the
customerInfo
node. AMPS uses the “leaf” nodes of the hierarchy for
comparison on the leaf nodes.
Creating Value Set Entries¶
The output of your parse function is a set of XPath entries contained in a XPath value set. This section describes the entries and presents the mechanics of creating XPath entries.
For each value in the message, you need to determine the following information:
- Tag This is the name of the value. For the tag, you need a pointer to the start of the tag and the length of the tag.
- Raw data Pointer and length for the raw data. This indicates the position in the message that holds the data for this value set entry.
- Type Type of the value. Within AMPS, all values are typed. It is up to your module to determine how to map the values in a message to the AMPS type system. See AMPS Values and Types for details on the AMPS type system.
- Native value If the value is numeric, the value represented as a
C native numeric type (
long
ordouble
) - String representation The string representation of the value. For some message types and values, this is the same as the raw data. However, if the data requires escaping, or if the raw data is not suitable for representation as a string, you may need to create a string representation. For example, if your message type sends numeric values as binary values rather than as strings, you can create a string equivalent.
- Index within the value set. This property is assigned after you add a new entry to the value set. You then use this index to provide AMPS information about this value’s place in the document hierarchy
For example, you might parse the following JSON document:
{
"messageId" : 5,
"customerInfo" : {
"customerId" : 20,
"notes" : "Have a nice day."
}
}
into the following value set entries:
Tag | Value | Type | Value Set Index (set by AMPS) | Parent | Next Sibling |
---|---|---|---|---|---|
messageId | 5 | Long | 1 | ~0 | 0 |
customerInfo | { “customerId” : 20, “notes” : “Have a nice day.” } | String | 2 | ~0 | 0 |
customerId | 20 | Long | 3 | 2 | 4 |
notes | Have a nice day. | String | 4 | 2 | 0 |
Table 9.5: Value Set Entries
Each of the required pieces of information is a property on the entry object.
Setting Numeric Values¶
The AMPS server SDK provides functions to help you create XPath entries. To create an entry, you decide the type of value (as described in section AMPS Values and Types. You then call the helper function appropriate for the type.
For example, a message type module that recognizes two numeric types,
long
and double
, might use the following code to set entries on
numeric values, based on whether the parser believes the value is a
long
or a double
:
// snippet to show sample use of
// amps_message_value_set_entry_set_<type> functions
// numStr is a C-style string that contains
// the value.
// entry is a pointer to amps_message_set_value_entry
// isDouble is a bool that indicates whether the module
// has parsed the value as a double or a long.
if (isDouble) {
double d = strtod(numStr, NULL);
amps_message_value_set_entry_set_double(entry, d);
}
else {
long l = strtol(numStr, NULL, 10);
amps_message_value_set_entry_set_long(entry, l);
}
Working With Hierarchical Documents¶
Value set entries are inherently hierarchical. This means that, for each
entry, AMPS requires you to set the identity of the parent node. If an
entry has no parent (that is, the entry is at the top level of the
message), AMPS requires you to set the parent to the special value
~0
.
If the entry has children or siblings, AMPS requires that you set the identity of those nodes, as well. If the entry has children or siblings, AMPS requires that you set the identity of those nodes, as well.
// Sample of setting parent and child nodes.
// root is the top of the document
// child_one is the first child, child_two is the second child
// All nodes are of type amps_message_value_set_entry, and
// are constructed by your parser as it parses.
root.parent = ~0;
// Add entry, child_one, and child_two to the value set
// and set the appropriate pointers.
uint32_t root_handle = amps_message_xpath_update(valueSet, findSet, &root);
uint32_t child_one_handle = amps_message_xpath_update(valueSet, findSet, &child_one);
uint32_t child_two_handle = amps_message_xpath_update(valueSet, findSet, &child_two);
// Now that we have the handles, use them to set up the relationships.
amps_message_xpath_get(valueSet, root_handle)->first_child = child_one_handle;
amps_message_xpath_get(valueSet, child_one_handle)->parent = root_handle;
amps_message_xpath_get(valueSet, child_one_handle)->next_sibling = child_two_handle;
amps_message_xpath_get(valueSet, child_two_handle)->parent = root_handle;
Representing Arrays¶
The AMPS type system does not provide a distinct type for arrays or
repeating values. Instead, repeating values are represented as a
collection of value objects with the same path at the same level of the
document hierarchy. To represent this, each value in the array has the
same parent
and the same hash
. The next_sibling
for each
element is set to point to the index for the next element.
Notice that this means that the following two JSON documents have approximately the same representation as parsed documents in AMPS:
{ "contents" : [ {"a" : 1 }, { "a" : 2 }, { "a" : 3} ] }
{ "contents" : "a" : [1, 2, 3] }
In both cases, the documents contain three values for the XPath identifier
/contents/a
. Those values are 1
, 2
, 3
, in order.
Implementing the Functions¶
AMPS requires a pair of functions to implement content-based routing. Each function receives an input message and a value set to populate with the parsed contents of the message. Once the value set is populated, or if message parsing fails, the function returns.
These functions produce identical results: the difference is that one function requests that your module parse the message completely, while the other function allows your module to partially parse the message – which can be more efficient for some message types.
To provide content filtering, you implement one or both of the following two functions:
int amps_message_type_parse_message(amps_input_message *inputMessage,
amps_message_xpath_value_set valueSet);
int amps_message_type_partial_parse_message(amps_input_message* inputMessage,
const amps_message_xpath_find_set findSet,
amps_message_xpath_value_set valueSet);
Each function takes the message provided in inputMessage
, and
populates the provided valueSet
with the parsed contents of the
message. Once a value_set_entry
is populated, you add the entry to
the valueSet using the provided amps_message_xpath_update
function.
For example, when entry is a populated value_set_entry
, a call with
the findSet
and valueSet
passed into partial_parse_message
might look like:
amps_message_xpath_update(valueSet, findSet, &entry);
The function returns the number of fields added to the valueSet
if
parsing succeeds, a negative value if parsing fails for any reason.
Notice that your code does not actually implement comparisons, queries
or filtering. AMPS uses the populated valueSet
to perform filtering
and queries over the messages.
Saving Parsed Values¶
AMPS has two mechanisms for saving user-defined state when working with messages. These are intended to be used for saving parsed values: while AMPS does not interpret the data you save in the messages, AMPS manages the lifetime of the data in a way that’s consistent with treating this data as parsed state.
Maintaining State for a Value Set¶
Some message types may be expensive to parse, and for those types, a message type module may need to parse the message once, and maintain a parsed representation of the message. AMPS allows modules to store user-defined data as a part of a value set and to retrieve this data from the value set. AMPS also provides a function so to release the reference to the data, so that the module can free the memory for the data.
To set user-defined data on a value set, use the
amps_message_xpath_set_user_data
function.
void amps_message_xpath_set_user_data(amps_message_xpath_value_set valueSet_, void* userData_);
As the signature implies, this function takes an
amps_message_xpath_value_set
and an arbitrary pointer, and stores
that pointer in the value set. To retrieve the value, use the
amps_message_xpath_get_user_data
function.
void* amps_message_xpath_get_user_data(amps_message_xpath_value_set valueSet_);
For example, an implementation of one of the parse functions might check to see whether the message has been previously parsed this way:
...
parsed_message_representation *msg = amps_get_user_data(valueSet);
if (0 == msg)
{
msg = custom_parser(inputMessage);
}
// use msg to update the valueSet
...
// store msg for possible future use
amps_message_set_user_data(valueSet, (void*)msg);
...
When a value set contains user data, AMPS calls
amps_message_type_free_user_data_function
with the data when AMPS no
longer uses it. For example, assuming that the parse_message
function given earlier has a corresponding free_message function to
release memory, the module might implement that function as follows:
void amps_message_type_free_user_data_function(void* msg)
{
// AMPS is done with the message, so release it.
free_message((parsed_message_representation*) msg);
}
Maintaining State for an Individual Value¶
AMPS also allows you to store state in an individual value. The
amps_message_value_set
structure contains three pointers that can be
used to save user data for that individual value. For example, your
module may want to store a pointer to an object representation of the
value. To do this, you simply set one of the user data pointers in the
value.
AMPS assumes that these pointers reference memory that is tracked
outside of the value itself, typically the parsed representation of the
message managed by set user data as described above. AMPS does not
provide a mechanism for freeing the memory referenced by an individual
value. Instead, AMPS expects that the memory is freed as a part of the
call to free_user_data
, or otherwise managed by the module.
Aggregation, Joins, Views and Delta Subscribe¶
To provide aggregation, joins, and views, AMPS must be able to construct messages from value sets. This process is the reverse of parsing process. In this case, AMPS provides a set of XPath identifiers with corresponding values, and your module creates an output message.
Your module implements the following pair of functions:
int amps_message_type_compute_serialize_size(amps_output_message_data *outputMessage);
int amps_message_type_serialize_message(amps_output_message_data *outputMessage);
AMPS calls these functions in sequence to guarantee that the serialization function can write the output message without requiring memory reallocation, regardless of the size of the message.
AMPS follows these steps:
AMPS first calls
amps_message_type_compute_serialize_size
with anamps_output_message_data
pointer that contains the value set to be serialized. When AMPS calls this function, the output pointer for theamps_output_message_data
points to a buffer of unknown size. Your implementation calculates the size of the message and sets this size in theserialize_size
member of the output message.Your module can do as much work as necessary to compute the size. If the work your module does would need to be repeated for serialization, you can save the results of that work in a set of
user_data
pointers held in theoutputMessage
. AMPS leaves theuser_data
pointers untouched when it provides theoutputMessage
to the serialization function. For example, if your module assembles a hierarchical document or escapes values, you can save the results of that work in theoutputMessage
and use that data for serialization.AMPS then allocates a buffer guaranteed to be large enough to hold the number of bytes returned by
amps_message_type_compute_serialize_size
. AMPS sets the output pointer of theoutputMessage
to point to that buffer.AMPS calls
amps_message_type_serialize_message
, providing the updatedoutputMessage
. Your module writes the serialized message to the output buffer.
The simple message type example in the SDK includes a complete implementation of these functions.
Notice that, in both cases, the provided outputMessage
holds the
following types of data, which your module assembles into a serialized
output message:
The headers set by the protocol. These are already written to the output message, and the length of the output message is set accordingly. Your module skips past these headers before it begins writing to the output message. You do not modify these headers, but your module must be aware that this data exists and avoid overwriting it.
Output serialized by other functions (such as delta message functions). This data is in the data message segment provided in the outputMessage. It has not been written to the output message. Your module decides how to write this data to the output message and increments the message length by the number of bytes written. For some message formats, this is a simple copy. For other message formats, your module may need to modify this data. For example, an XML-based message type may need to write the unserialized values before the closing tag in this data.
Unserialized values. Unserialized values are held in an array of
amps_message_value
objects pointed to by the values member of the output message. Your serialize function serializes this data to the output message, and increments the message length by the number of bytes written.Correctly serializing unserialized values involves a set of common practices across all message types, and is discussed in more detail below.
Serialized messages contain all three types of data. The full sample module in the SDK implements a serializer that correctly handles all three types of data.
Working with Unserialized Values¶
For unserialized values, your module effectively reverses the parsing process. AMPS provides a set of identifiers and values, and you assemble these values into a message.
AMPS provides unserialized values as instances of the
amps_message_value
struct. Conceptually, a message value is the
combination of an XPath-based identifier or tag, the value of that
identifier, and the information necessary to efficiently and correctly
serialize the value.
Most values are the result of parsing a published message. These values contain an XPath-based identifier, the tag, to be serialized and the value of the tag. The values provided are the result of parsing the message, as performed by the message type module for the source message.
Other values are provided by AMPS. AMPS generates these values for the
status messages AMPS publishes to the /AMPS/.*
family of topics.
When a value is provided by AMPS, the tag is empty and zero-length.
Instead, AMPS provides a constant that your module uses to look up a
static string that you define as the name of the tag. Your module looks
up the name, formats the tag appropriately, and writes the value.
Serialization Example¶
For example, consider the hierarchical message discussed above.
XPath-based identifier | Value |
---|---|
/messageId | 5 |
/customerInfo/customerId | 20 |
/customerInfo/notes | Have a nice day. |
Table 9.6: Hierarchical Format Example Identifiers and Values
Your module serializes this in the appropriate format for your module. Given the set of tags and values above, a JSON module might serialize the message as follows:
{
"messageId" : 5,
"customerInfo" : {
"customerId" : 20,
"notes" : "Have a nice day."
}
}
An XML module might serialize the message this way:
<AMPSMessage>
<messageId>5</messageId>
<customerInfo>
<customerId>5</customerId>
<notes>Have a nice day.</notes>
</customerInfo>
</AMPSMessage>
A module that does simple, flat serialization might serialize the message with an implied hierarchy:
messageId=5, customerInfo/customerId=20, customerInfo/notes=Have a nice day.
Notice that your module may receive values that were parsed by another message type. This can happen when, for example, messages from multiple topics are joined together to project a view. Therefore, even if your message type only parses messages with a certain layout (such as messages without a hierarchical structure), your message type may need to serialize messages with a different layout.
Delta Publish¶
For a delta publish, AMPS needs to be able to receive an update to a message, and merge that update into an existing message to form a complete message. For delta publish, AMPS uses the following process:
- Parse incoming message by calling
amps_message_type_parse_message
oramps_message_type_partial_parse_message
with the incoming message. Use the value set provided to locate the existing message in the SOW, if one exists. - Parse the existing message by calling
amps_message_type_parse_message
oramps_message_type_partial_parse_message
. - Merge the parsed messages by calling
amps_message_type_delta_publish_merge
. - Persist the merged message to the SOW, and publish to subscribers as necessary.
The function that AMPS calls to merge messages has the following signature:
int amps_message_type_delta_publish_merge(amps_delta_message_data *pDeltaMessage_);
Merging Messages for Delta Publish¶
When merging messages, AMPS provides the information for both the original message and an update message. Your module is responsible for creating a serialized message that contains the updated values merged with values that are unchanged from the original message.
AMPS provides your module with a structure that contains the update message, the parsed value set for the update message, the existing message that is being updated, and the parsed value set for the message that is being updated. Your module serializes the unchanged values and adds entries for these values to the value set in the update message.
One simple way to do this is to use the following process to merge messages:
- Skip past the header information and the new values that have been published in the update.
- Serialize the existing values that have not been replaced by new values. Your module determines this by checking whether the identifier for the existing value is present in the new value set. If so, the message already contains an updated value.
- Add the existing values to the value set for the updated message.
- When all of the existing values have been processed, return the new length of the serialized message.
Following this process, your module creates a merged message that contains values from both the new message and the existing values. For more complicated message formats, such as XML, your module may write the old values & new values into an intermediate format and then serialize the intermediate format.
The following example shows one way to merge messages:
int amps_message_type_delta_publish_merge(amps_delta_message_data *pDeltaMessage_)
{
// Get the new value set and the old value set.
amps_message_xpath_value_set newValues = pDeltaMessage_->update_value_set;
amps_message_xpath_value_set oldValues = pDeltaMessage_->source_value_set;
// Get pointers to the message data.
const char *updateMessageData = pDeltaMessage_->update_data->data;
const size_t updateMessageDataLength = pDeltaMessage_->update_data->length;
const char *oldMessageData = amps_message_xpath_get_value_data(oldValues);
// Start writing after anything that's already been written, such as headers.
// Notice that AMPS has already reserved space for the message.
char *output = pDeltaMessage_->output.data + pDeltaMessage_->output.byte_count;
char *origin = output;
// Copy the update message verbatim. Notice that, for more complicated message
// types such as JSON or XML, you may need to do a true merge.
__builtin_memcpy(output, updateMessageData, updateMessageDataLength);
output += updateMessageDataLength;
// Write old values that have not been replaced by new values,
// and add those values to the value set.
for (amps_message_value_set_entry* entry =
amps_message_xpath_begin(oldValues);
entry != amps_message_xpath_end(oldValues);
++entry)
{
if (entry->hash == 0) continue;
size_t valueIndex = amps_message_xpath_find_by_hash(newValues, entry->hash);
// continue if there's an entry in the new values.
amps_message_value_set_entry *pValue = amps_message_xpath_get(newValues,valueIndex);
if (valueIndex != amps_message_value_invalid_index && pValue != NULL && pValue->hash != 0)
{
continue;
}
amps_message_value_set_entry updateValue = *entry;
// serialize function (not shown) writes to output and returns
// the number of characters written. This can be simple for
// simple message types, or complicated for highly-structured
// message types.
updateValue.tag_bytes = entry->tag_bytes;
updateValue.tag_length = entry->tag_length;
output += serialize_tag(output, oldMessageData + entry->tag_offset, entry->tag_length);
updateValue.string_rep = entry->string_rep;
updateValue.string_rep_length = entry->string_rep;
updateValue.raw_bytes = entry->raw_bytes;
updateValue.raw_bytes_length = entry->raw_bytes_length;
output += serialize_value(output, oldMessageData + entry->offset, entry->length);
// add the value to the value set
amps_message_xpath_update(newValues, NULL, &updateValue);
}
// set the length of the output message.
pDeltaMessage_->output.byte_count += (output - origin);
return pDeltaMessage_->output.byte_count;
}
Providing Readable Messages for Tracing¶
By default, AMPS includes the verbatim bytes of a message when logging the message. This is a good option for message types that are designed to be easily readable.
If your message type is not easily readable (for example, your message type includes binary data), AMPS provides an interface that allows you to produce a readable version of a message for use in AMPS logging. When your message type module implements this function, AMPS calls the function with a serialized message whenever AMPS needs to produce a readable version of a message.
There is no requirement that you implement this function. If not provided, AMPS logs the verbatim bytes of the message.
The function has the following signature:
int amps_message_type_trace_message(amps_message_type_context context_,
const amps_trace_buffer* pTraceBuffer_,
amps_trace_output_buffer* pOutputBuffer_);
Your module reads the serialized message provided in the trace buffer, creates a readable version of the message, and copies the readable version of the message into the trace output buffer.
The output buffer contains a pointer (output
) to an area of
pre-allocated memory, with the size of that area represented by the
capacity
of the output buffer. Notice that the size of the buffer
that is available for your module to use is capacity - length
, as
the buffer may already contain data. Your module begins writing data at
output + length
, writes the necessary data, and then updates the
value of the length
to reflect the number of bytes written.
In the event that the available capacity of the buffer is not sufficient
to hold the readable format of the message, return AMPS_FAILURE
from
the function. AMPS will interpret this as a request for more space in
the buffer and call the function again with a larger buffer.
A module should not return AMPS_FAILURE
from this function if the
message cannot be meaningfully serialized (for example, if an incoming
message has corrupt or invalid data). Instead, the module should record
the reason for the failure in the buffer and return AMPS_SUCCESS
.