Chapter 11: Direct use of an InstanceIdentifier#
In all previous chapters we always used an InstanceSpecifier to address a service instance – be it for finding a
service (FindService/StartFindService) or for creating a skeleton. In this chapter we look at the
InstanceIdentifier and a special – sometimes called power-user – use case where we use it directly. Along the
way we build a small asynchronous queue-and-callback communication pattern.
InstanceSpecifier vs. InstanceIdentifier#
As already explained in chapter 1, an InstanceSpecifier is basically a string that
serves as a lookup key into the score::mw::com configuration. It points at a concrete service instance in the
serviceInstances array – namely the one whose instanceSpecifier property matches. The configuration stored behind
that key (the service-type deployment plus the instance deployment) makes up the InstanceIdentifier.
There are exactly three usages of an InstanceSpecifier in the public score::mw::com API:
Service instance search (
<ProxyClass>::FindServiceor<ProxyClass>::StartFindService)Skeleton creation
score::mw::com::runtime::ResolveInstanceIDs
The first two are the common ones we have been using since chapter 1. The third one is very specific and is the subject
of this chapter. Internally, the search and skeleton-creation APIs resolve the InstanceSpecifier into an
InstanceIdentifier (containing all configuration aspects) and then dispatch to overloads that directly take an
InstanceIdentifier. In other words: the InstanceSpecifier overloads are just short-cuts that first do the
InstanceSpecifier → InstanceIdentifier resolution and then call the InstanceIdentifier overload.
Note
Surprisingly, a service instance in the configuration has an array-type property instances. This is also the
reason why ResolveInstanceIDs returns a collection of InstanceIdentifier``s. In the current state of
``score::mw::com we do not make use of this array semantics: the instances array always contains exactly one
element. The idea behind allowing potentially multiple “technical” instances behind one “abstract” service instance is
called multi-binding (e.g. providing the same service both over a local IPC binding like LoLa/SHM and over a
network binding). For this tutorial we simply ignore the multi-binding case and always expect exactly one
InstanceIdentifier.
Why would you ever use the InstanceIdentifier overloads directly?#
The short answer: if the InstanceIdentifier does not exist in the configuration of your score::mw::com application!
If it is in your configuration, you would simply use its InstanceSpecifier – that is more convenient. But an
InstanceIdentifier is serializable: it can be turned into a string (via ToString()), sent to some recipient
(e.g. another process), and there be turned back into an InstanceIdentifier (via InstanceIdentifier::Create()) –
without that recipient having the instance in its own configuration. This is exactly what we exploit here.
The use case: turning a long-running synchronous method into an asynchronous queue-and-callback#
Imagine a service that offers a very long-running / work-intensive method. In the current state of score::mw::com, a
method call on a ProxyMethod is blocking – so calling such a method would block the consumer for a long time. The
consumer could work around this by dispatching the call to a separate thread, or wait for a future score::mw::com
extension offering an asynchronous ProxyMethod call API. But there is a third option that involves a redesign of
the service interface:
The (long running) method is transformed into a (short running) method whose only job is to queue the task for execution (a semantic change!). When this redesigned method returns, it only signals that the task has been queued at the provider side. The actual result (or just the notification that the operation finished) is delivered back to the caller later, in the form of a callback.
For this to work:
The consumer/caller itself has to provide a service instance implementing a service interface that defines this callback method.
When the provider has finished the queued operation for a specific consumer, it needs to call the callback method on exactly the service instance provided by that consumer.
Therefore the provider needs some information, within the initial (queueing) method call, that lets it find the right instance of this callback service.
That last bullet is the actual challenge. In reality the provider could have many different consumers, each providing its
own instance of the callback service. So the provider cannot just do a “find-any” search for the callback service –
it would get many handles back and would not know which one to use. One could solve this with additional complexity
(each consumer hands over a unique id, the callback interface gets an extra GetId() method, the provider does a
find-any search, creates a proxy for each hit, calls GetId() on each until it finds the matching one, and releases
the rest). That is doable, but complex and inefficient.
What the provider really wants is a search for one specific instance – and the direct use of an
InstanceIdentifier gives exactly that. Here is how we solve it:
The queueing method gets an additional parameter (a fixed-string) representing the serialized InstanceIdentifier of the consumer’s callback service instance.
Before calling the provider’s queueing method, the consumer instantiates a skeleton for the callback interface and calls
OfferService()on it.The consumer then calls the provider’s queueing method, setting the fixed-string to the serialized form of the
InstanceIdentifierof the skeleton it created in step 2. It obtains that serialized form by callingscore::mw::com::runtime::ResolveInstanceIDswith theInstanceSpecifierused in step 2 (which returns exactly oneInstanceIdentifier) and then callingToString()on it.The provider, when receiving the queueing call, also gets the string representation of the callback
InstanceIdentifier. It creates anInstanceIdentifierfrom it (viaInstanceIdentifier::Create()) and does aFindServicebased on thisInstanceIdentifier.This returns exactly one handle, from which the provider creates the matching callback proxy instance.
When the queued task finishes, the provider calls the callback method on that callback service instance to notify the consumer that the asynchronous job has finished.
The service interfaces#
We define two service interfaces in long_running_service.h.
The provider provides a LongRunningServiceInterface. It contains a single method PostLongRunningJob with three
in-arguments and a bool result (reflecting whether the job could be queued):
a
std::uint32_t– a dummy value standing in for a hypothetical job argument,a
std::uint32_t– the job number; the caller hands this over and expects to get it back in theSetJobResultmethod of the callback interface,a fixed-string large enough to hold an
InstanceIdentifier::ToString()representation.
The callback service is LongRunningServiceResultInterface. It contains a single method SetJobResult with two
in-arguments and no result (a void method):
a
std::uint32_t– a dummy value representing the job result,a
std::uint32_t– the job number the result belongs to (it correlates with the job number of thePostLongRunningJobcall).
The fixed-string type used to transport the serialized InstanceIdentifier is a plain std::array<char, N> (method
arguments are transported through shared memory and must be trivially copyable). We choose a generous capacity; the
consumer asserts at runtime that the serialized form actually fits:
using SerializedInstanceIdentifier = std::array<char, 2048>;
static_assert(std::is_trivially_copyable_v<SerializedInstanceIdentifier>,
"SerializedInstanceIdentifier must be trivially copyable to be usable as a method argument.");
The LongRunningServiceInterface:
template <typename Trait>
class LongRunningServiceInterface : public Trait::Base
{
public:
using Trait::Base::Base;
// PostLongRunningJob(job_argument, job_number, serialized_callback_instance_identifier) -> queued_successfully
// - job_argument: a dummy value standing in for the actual job input.
// - job_number: chosen by the caller; echoed back in the SetJobResult() callback so the caller can correlate a
// result with the request that produced it.
// - serialized_callback_instance_identifier: InstanceIdentifier::ToString() of the consumer's callback instance.
// - return: true if the job was queued successfully, false otherwise.
typename Trait::template Method<bool(std::uint32_t, std::uint32_t, SerializedInstanceIdentifier)>
post_long_running_job_{*this, "PostLongRunningJob"};
};
The LongRunningServiceResultInterface:
template <typename Trait>
class LongRunningServiceResultInterface : public Trait::Base
{
public:
using Trait::Base::Base;
// SetJobResult(job_result, job_number) -> void
// - job_result: a dummy value standing in for the actual job result.
// - job_number: the job_number of the PostLongRunningJob() call this result belongs to.
typename Trait::template Method<void(std::uint32_t, std::uint32_t)> set_job_result_{*this, "SetJobResult"};
};
And the proxy/skeleton aliases for both interfaces:
using LongRunningServiceProxy = score::mw::com::AsProxy<LongRunningServiceInterface>;
using LongRunningServiceSkeleton = score::mw::com::AsSkeleton<LongRunningServiceInterface>;
using LongRunningServiceResultProxy = score::mw::com::AsProxy<LongRunningServiceResultInterface>;
using LongRunningServiceResultSkeleton = score::mw::com::AsSkeleton<LongRunningServiceResultInterface>;
Files/artifacts used#
The bazel project for this chapter is located in score/mw/com/doc/tutorial/chapter_11 and contains the following
files:
File Name |
Description |
|---|---|
This file contains bazel targets for this example. |
|
Implementation of the service consumer app. The main() for the consumer. |
|
Header (empty - we just always want to have cpp/h pairs) of the service consumer. |
|
This file contains the configuration for score::mw::com for the consumer app. |
|
This file contains the configuration for the logging system used by score::mw::com |
|
Implementation of the service provider. |
|
Header (empty) of the service provider. |
|
This file contains the configuration for score::mw::com for the provider app. |
|
This file contains the configuration for the logging system used by score::mw::com |
|
This file is empty as the service interfaces are completely defined in the header. |
|
This file contains the definition of the two service interfaces. |
Configuration#
The provider configuration declares only the LongRunningService – the service type and the single instance
MyLongRunningServiceInstance that it provides. Note that it does not declare the callback service
(LongRunningServiceResult) at all – that is the whole point of this chapter: the provider will address the callback
instance purely via the InstanceIdentifier it receives at runtime.
{
"serviceTypes": [
{
"serviceTypeName": "/score/mw/com/tutorial/LongRunningService",
"version": {
"major": 1,
"minor": 0
},
"bindings": [
{
"binding": "SHM",
"serviceId": 8715,
"methods": [
{
"methodName": "PostLongRunningJob",
"methodId": 1
}
]
}
]
}
],
"serviceInstances": [
{
"instanceSpecifier": "MyLongRunningServiceInstance",
"serviceTypeName": "/score/mw/com/tutorial/LongRunningService",
"version": {
"major": 1,
"minor": 0
},
"instances": [
{
"instanceId": 1,
"asil-level": "QM",
"binding": "SHM",
"methods": [
{
"methodName": "PostLongRunningJob",
"queueSize": 1
}
]
}
]
}
],
"global": {
"asil-level": "QM",
"applicationID": 50
}
}
The consumer configuration declares both services: the LongRunningServiceResult (the callback service –
service type plus the instance MyLongRunningServiceResultInstance it provides/offers) and the
LongRunningService (service type plus the instance MyLongRunningServiceInstance it consumes; because a method is
called on the proxy side, the method is listed with a queueSize). The serviceId``s and ``methodId``s of the
``LongRunningService must match between provider and consumer.
{
"serviceTypes": [
{
"serviceTypeName": "/score/mw/com/tutorial/LongRunningService",
"version": {
"major": 1,
"minor": 0
},
"bindings": [
{
"binding": "SHM",
"serviceId": 8715,
"methods": [
{
"methodName": "PostLongRunningJob",
"methodId": 1
}
]
}
]
},
{
"serviceTypeName": "/score/mw/com/tutorial/LongRunningServiceResult",
"version": {
"major": 1,
"minor": 0
},
"bindings": [
{
"binding": "SHM",
"serviceId": 8716,
"methods": [
{
"methodName": "SetJobResult",
"methodId": 1
}
]
}
]
}
],
"serviceInstances": [
{
"instanceSpecifier": "MyLongRunningServiceInstance",
"serviceTypeName": "/score/mw/com/tutorial/LongRunningService",
"version": {
"major": 1,
"minor": 0
},
"instances": [
{
"instanceId": 1,
"asil-level": "QM",
"binding": "SHM",
"methods": [
{
"methodName": "PostLongRunningJob",
"queueSize": 1
}
]
}
]
},
{
"instanceSpecifier": "MyLongRunningServiceResultInstance",
"serviceTypeName": "/score/mw/com/tutorial/LongRunningServiceResult",
"version": {
"major": 1,
"minor": 0
},
"instances": [
{
"instanceId": 1,
"asil-level": "QM",
"binding": "SHM",
"methods": [
{
"methodName": "SetJobResult",
"queueSize": 1
}
]
}
]
}
],
"global": {
"asil-level": "QM",
"applicationID": 51
}
}
Provider and consumer are two separate applications, so they get different applicationID``s (``50 and 51).
Provider application#
Like the earlier provider examples, the provider relies on the runtime’s implicit, lazy initialization – there is no
explicit InitializeRuntime call. It registers the PostLongRunningJob handler and offers the service. The handler
receives the bool return
value as its first out-parameter (by reference), followed by the three input arguments by const-reference. Crucially, it
does not run the job synchronously: it only makes sure the callback InstanceIdentifier is deserialized/cached and
then arms a timer. The FindService/proxy-creation and the actual callback call happen later, on the timer thread:
// actual FindService/proxy creation and the callback call happen later, on the timer thread (outside this
// middleware handler thread). Handlers MUST be registered before OfferService.
const auto handler_result = service_instance.post_long_running_job_.RegisterHandler(
[](bool& queued,
const std::uint32_t& job_argument,
const std::uint32_t& job_number,
const SerializedInstanceIdentifier& serialized_callback_id) -> void {
const std::string serialized_instance_id = ToSerializedString(serialized_callback_id);
std::cout << "PostLongRunningJob(job_argument=" << job_argument << ", job_number=" << job_number
<< ") received. Resolving callback instance ..." << std::endl;
if (!EnsureCallbackIdentifier(serialized_instance_id))
{
std::cerr << "Could not deserialize the callback InstanceIdentifier for job_number=" << job_number
<< ". Job NOT queued." << std::endl;
queued = false;
return;
}
// Arm a "timer" that fires after a random delay between 1s and 10s, then delivers the result via the
// callback. We simply use a detached thread that sleeps. On first fire it lazily creates (and caches) the
// callback proxy for the given serialized InstanceIdentifier; subsequent jobs reuse the cached proxy.
std::thread{[serialized_instance_id, job_number]() {
std::mt19937 random_engine{std::random_device{}()};
const std::chrono::seconds delay{std::uniform_int_distribution<std::uint32_t>{1U, 10U}(random_engine)};
std::this_thread::sleep_for(delay);
auto callback_proxy = GetOrCreateCallbackProxy(serialized_instance_id);
if (callback_proxy == nullptr)
{
std::cerr << "Could not obtain a callback proxy for job_number=" << job_number
<< ". Result NOT delivered." << std::endl;
return;
}
const std::uint32_t job_result = std::uniform_int_distribution<std::uint32_t>{}(random_engine);
std::cout << "Long running job_number=" << job_number << " finished (result=" << job_result
<< "). Calling SetJobResult() on the consumer's callback instance ..." << std::endl;
std::lock_guard<std::mutex> lock{g_callback_call_mutex};
const auto call_result = callback_proxy->set_job_result_(job_result, job_number);
if (!call_result.has_value())
{
std::cerr << "SetJobResult() call for job_number=" << job_number
<< " failed: " << call_result.error() << std::endl;
}
}}.detach();
queued = true;
Note
InstanceIdentifier::Create() (used further below) only works once the runtime configuration has been locked,
i.e. after the runtime singleton has been constructed. Here this is always the case by the time the handler runs,
because creating the skeleton and calling OfferService() above already triggered the (implicit) runtime
initialization.
Note
We deliberately do the FindService/proxy creation on the timer thread, not inside the method handler. The
method handler runs on an internal middleware thread; performing the callback proxy setup (which itself needs
message-passing round-trips to the consumer) from within that handler does not work reliably. Doing it on a separate
thread – after the handler has already returned queued = true to the consumer – keeps the request path short and
the setup robust.
The direct InstanceIdentifier use is concentrated in two helpers. EnsureCallbackIdentifier deserializes the
callback InstanceIdentifier exactly once per distinct serialized form (calling InstanceIdentifier::Create()
more than once for the same service-type deployment would terminate the process, because deserialization registers that
deployment in the runtime configuration):
bool EnsureCallbackIdentifier(const std::string& serialized_instance_id)
{
std::lock_guard<std::mutex> lock{g_callback_cache_mutex};
if (g_callback_identifiers.find(serialized_instance_id) != g_callback_identifiers.end())
{
return true;
}
auto instance_identifier_result = score::mw::com::InstanceIdentifier::Create(std::string{serialized_instance_id});
if (!instance_identifier_result.has_value())
{
std::cerr << "Failed to create InstanceIdentifier from serialized string: "
<< instance_identifier_result.error() << std::endl;
return false;
}
g_callback_identifiers.emplace(serialized_instance_id, std::move(instance_identifier_result).value());
return true;
}
GetOrCreateCallbackProxy then uses the (already deserialized and cached) InstanceIdentifier to do a targeted
FindService and creates/caches the proxy. Because the InstanceIdentifier fully describes the callback instance,
this returns exactly the one matching handle – no ambiguous “find-any” search is needed. The proxy is cached and reused
for all subsequent jobs of the same consumer:
std::shared_ptr<LongRunningServiceResultProxy> GetOrCreateCallbackProxy(const std::string& serialized_instance_id)
{
std::lock_guard<std::mutex> lock{g_callback_cache_mutex};
const auto cached = g_callback_proxies.find(serialized_instance_id);
if (cached != g_callback_proxies.end())
{
return cached->second;
}
const auto identifier = g_callback_identifiers.find(serialized_instance_id);
if (identifier == g_callback_identifiers.end())
{
std::cerr << "No cached InstanceIdentifier for the callback instance." << std::endl;
return nullptr;
}
// A targeted, synchronous find for exactly this callback instance identifier (using a copy of the cached
// identifier).
auto find_result =
LongRunningServiceResultProxy::FindService(score::mw::com::InstanceIdentifier{identifier->second});
if (!find_result.has_value())
{
std::cerr << "FindService for the callback instance identifier failed: " << find_result.error() << std::endl;
return nullptr;
}
if (find_result.value().empty())
{
std::cerr << "FindService for the callback instance identifier returned no handle." << std::endl;
return nullptr;
}
auto proxy_result = LongRunningServiceResultProxy::Create(find_result.value().front());
if (!proxy_result.has_value())
{
std::cerr << "Failed to create callback proxy: " << proxy_result.error() << std::endl;
return nullptr;
}
auto proxy = std::make_shared<LongRunningServiceResultProxy>(std::move(proxy_result).value());
g_callback_proxies.emplace(serialized_instance_id, proxy);
std::cout << "Created and cached a new callback proxy for a consumer's callback instance." << std::endl;
Like in the earlier provider examples, main() has no while-loop: after offering the service it simply blocks on a
condition variable until a termination signal requests the shut down.
Consumer application#
The consumer first creates, configures and offers its own callback service instance
(LongRunningServiceResultInstance). It registers the SetJobResult handler (a void method, so the handler only
receives the two input arguments and just logs them) before offering the service:
auto result_skeleton = std::move(result_skeleton_result).value();
// Register the SetJobResult() handler. It has no return value (void method), so the handler only receives the two
// input arguments by const-reference. It simply logs the delivered result together with its job_number. Handlers
// MUST be registered before OfferService().
const auto set_result_handler_result = result_skeleton.set_job_result_.RegisterHandler(
[](const std::uint32_t& job_result, const std::uint32_t& job_number) -> void {
std::cout << "Callback SetJobResult received: job_number=" << job_number << " has result=" << job_result
<< "." << std::endl;
});
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(set_result_handler_result.has_value(),
"Failed to register 'SetJobResult' method handler!");
auto result_offer_result = result_skeleton.OfferService();
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(result_offer_result.has_value(),
"Failed to offer LongRunningServiceResultSkeleton instance!");
std::cout << "Callback service instance offered." << std::endl;
It then resolves the InstanceIdentifier of that callback instance via ResolveInstanceIDs, expects exactly one
result, and serializes it (via ToString()) into the fixed-size buffer it will pass in every PostLongRunningJob
call:
// 2. Resolve the InstanceIdentifier of our just-offered callback instance and serialize it. ResolveInstanceIDs()
// performs the InstanceSpecifier -> InstanceIdentifier lookup in our configuration; we expect exactly ONE
// InstanceIdentifier (the "instances" array holds exactly one element - see the chapter text). Its ToString()
// representation is what we hand to the provider so it can later find *this* callback instance.
auto resolve_result = score::mw::com::runtime::ResolveInstanceIDs(result_specifier.value());
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(resolve_result.has_value(),
"Failed to resolve callback InstanceIdentifier!");
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(resolve_result.value().size() == 1U,
"Expected exactly one InstanceIdentifier for the callback instance!");
const std::string_view serialized_callback_id_view = resolve_result.value().front().ToString();
const SerializedInstanceIdentifier serialized_callback_id =
SerializeCallbackInstanceIdentifier(serialized_callback_id_view);
std::cout << "Serialized callback InstanceIdentifier (" << serialized_callback_id_view.size()
<< " bytes) prepared for PostLongRunningJob calls." << std::endl;
Next it does a StartFindService-based search for the LongRunningService instance provided by our provider and,
once found, creates a proxy for it:
// 3. Discover the LongRunningService and create a proxy for it.
auto service_specifier = score::mw::com::InstanceSpecifier::Create(std::string{kServiceInstanceSpecifierString});
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(service_specifier.has_value(),
"Failed to create service InstanceSpecifier!");
std::optional<LongRunningServiceProxy> proxy{};
auto find_service_handler = [&proxy](
score::mw::com::ServiceHandleContainer<LongRunningServiceProxy::HandleType> handles,
score::mw::com::FindServiceHandle find_service_handle) noexcept {
score::cpp::ignore = LongRunningServiceProxy::StopFindService(find_service_handle);
std::cout << "Found LongRunningService instance. Creating proxy." << std::endl;
auto proxy_result = LongRunningServiceProxy::Create(handles.front());
if (!proxy_result.has_value())
{
std::cerr << "Failed to create LongRunningService proxy: " << proxy_result.error() << ". Terminating."
<< std::endl;
std::exit(EXIT_FAILURE);
}
{
std::lock_guard<std::mutex> lock{g_proxy_ready_mutex};
proxy.emplace(std::move(proxy_result).value());
}
g_proxy_ready_cv.notify_one();
};
auto find_service_handle_result =
LongRunningServiceProxy::StartFindService(std::move(find_service_handler), service_specifier.value());
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE(find_service_handle_result.has_value(),
"Failed to start service discovery for LongRunningService!");
Finally it cyclically (every 500 ms) calls PostLongRunningJob on the proxy – setting the dummy argument to a
random number and the job number to a value starting at 0 and incremented for each call – until it receives a
kill/shutdown signal. The results are delivered asynchronously through the SetJobResult callback handler registered
above:
// 4. Cyclically post long running jobs until a shutdown is requested. The results are delivered asynchronously via
// the SetJobResult() callback handler registered above (invoked on an internal middleware thread).
if (proxy.has_value())
{
std::mt19937 random_engine{std::random_device{}()};
std::uniform_int_distribution<std::uint32_t> value_distribution{};
std::uint32_t job_number{0U};
while (!g_shutdown_requested.load(std::memory_order_relaxed))
{
const std::uint32_t job_argument = value_distribution(random_engine);
std::cout << "Posting long running job_number=" << job_number << " (job_argument=" << job_argument << ")."
<< std::endl;
auto call_result = proxy->post_long_running_job_(job_argument, job_number, serialized_callback_id);
if (!call_result.has_value())
{
std::cerr << "PostLongRunningJob call failed: " << call_result.error() << std::endl;
}
else
{
std::cout << " PostLongRunningJob(job_number=" << job_number
<< ") queued at provider: " << std::boolalpha << *(call_result.value()) << std::endl;
}
++job_number;
std::unique_lock<std::mutex> lock{g_shutdown_mutex};
g_shutdown_cv.wait_for(lock, kPostCycleTime, [] {
return g_shutdown_requested.load(std::memory_order_relaxed);
});
}
}
How to run the example#
First build the two applications:
# Build the provider and consumer targets
bazel build //score/mw/com/doc/tutorial/chapter_11:provider-tar
bazel build //score/mw/com/doc/tutorial/chapter_11:consumer-tar
Extract both archives (e.g. in a tmp-directory):
mkdir -p /tmp/tutorial/chapter_11
tar -xf <project dir>/bazel-bin/score/mw/com/doc/tutorial/chapter_11/provider-tar.tar -C /tmp/tutorial/chapter_11/
tar -xf <project dir>/bazel-bin/score/mw/com/doc/tutorial/chapter_11/consumer-tar.tar -C /tmp/tutorial/chapter_11/
Start the service-provider application in the 1st terminal:
cd /tmp/tutorial/chapter_11/opt/LongRunningServer
bin/provider_app
… and the service-consumer in the 2nd terminal:
cd /tmp/tutorial/chapter_11/opt/LongRunningClient
bin/consumer_app
You will observe the consumer posting jobs every 500 ms (each getting queued at provider: true), and – after a random
1-10 s delay per job – the corresponding SetJobResult callbacks arriving asynchronously, each carrying the correct
job_number back:
Posting long running job_number=0 (job_argument=2586070691).
PostLongRunningJob(job_number=0) queued at provider: true
...
Callback SetJobResult received: job_number=5 has result=1986113591.
Callback SetJobResult received: job_number=1 has result=1559372926.
...
Note how the callbacks do not arrive in job-number order – each job’s timer has an independent random delay, so the results come back in a different order than the jobs were posted. This nicely illustrates the asynchronous queue-and-callback nature of the pattern.
Note
For simplicity the provider uses one detached timer thread per job that may still be pending when the process is asked to shut down; the process just exits in that case. This is an acceptable tutorial simplification – a production implementation would manage those timers explicitly.
Summary#
A short summary of what we have learned in this chapter:
An InstanceSpecifier is a lookup key into the configuration; the configuration behind it makes up an InstanceIdentifier. The
FindService/StartFindServiceand skeleton-creation APIs internally resolve theInstanceSpecifierto anInstanceIdentifierand provide overloads that take theInstanceIdentifierdirectly.score::mw::com::runtime::ResolveInstanceIDsperforms this resolution explicitly and returns a collection ofInstanceIdentifier``s (the ``instancesarray; always exactly one element in the current state – the multi-binding case is not used here).The direct-
InstanceIdentifieroverloads are useful when theInstanceIdentifieris not in your own configuration. AnInstanceIdentifieris serializable (ToString()) and can be reconstructed elsewhere viaInstanceIdentifier::Create()– even in an application that does not have that instance in its configuration.We used this to implement an asynchronous queue-and-callback pattern: the consumer offers a callback service instance, serializes its
InstanceIdentifier, and passes it in the queueing method call; the provider deserializes it, does a targetedFindService, caches the resulting callback proxy, and later delivers the result via the callback method – addressing exactly the one callback instance that belongs to that consumer.Practical caveats: call
InstanceIdentifier::Create()once per distinct identifier (cache the result), do theFindService/proxy setup outside the method-handler thread, and remember thatInstanceIdentifier::Create()only works after the runtime configuration has been locked (which the implicit runtime initialization – triggered by skeleton creation /OfferService– already takes care of here).