Repository navigation
[DO NOT MERGE] Spike having a federation layer on top of multiple instances of ServiceControl - #6020
Open
SzymonPobiega wants to merge 5 commits into
Open
SzymonPobiega wants to merge 5 commits into
SzymonPobiega wants to merge 5 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ServiceControl aggregating proxy: design proposal
Status: draft, reviewed once. An initial implementation exists in
src/ServiceControl.Federation(see §14).Decisions from the first review:
RemoteInstances(§14). An earlier standalone host was dropped.1. Problem and goal
One ServicePulse instance today talks to exactly one ServiceControl primary (error) instance and one ServiceControl.Monitoring instance. Organisations that run several independent ServiceControl installations (per region, per business unit, per broker) need one ServicePulse per installation.
Goal: a C# component, called the aggregating proxy below, that:
/api/...) and as ServiceControl.Monitoring, so ServicePulse is not modified;Non-goals:
Terminology
{SC}/{MU}2. Inputs to this design
This proposal is based on reading the code, not on running anything.
src/ServiceControl.UnitTests/ApprovalFiles/APIApprovals.HttpApiRoutes.approved.txt. That file omits the/apiprefix,api/authentication/configuration,api/maintenance/retention/purge, and the licensing component'sapi/licensing/*routes. Those were taken from the controllers.APIApprovals.HttpApiRoutes.approved.txtfiles.src/ServiceControl/CompositeViews/Messages/ScatterGatherApi.cs, and how ETags are combined in docs/data-versioning-design.md.src/Frontend/srcin the ServicePulse repository. All HTTP calls go throughcomponents/serviceControlClient.tsandcomponents/monitoring/monitoringClient.ts.Key facts that shape the design
ServicePulse knows only two base URLs: one
{SC}(ending in/api/) and one{MU}. Every call is{SC}or{MU}plus a relative suffix. Putting the proxy at those two URLs is enough./api/**and/monitoring-api/**with YARP (src/ServicePulse/ReverseProxy.cs), so the proxy can sit behind it.There is no push channel. SignalR was removed from ServiceControl. ServicePulse polls everything, mostly every 5 s and down to 1 s during operations (§10.1).
ServicePulse already handles partial results. It parses
X-Particular-Incomplete-Results: <instanceId>:<timeout|unavailable|error>, …onmessages2(components/incompleteResults.ts).Only two server-provided URLs are followed:
my_routes_urlfrom the root document, which is absolute.body_url, which is relative,/messages/{id}/body?instance_id=…, and resolved against{SC}.All other URLs in the root document are stored but never fetched.
One header drives every feature gate:
X-Particular-VersiononGET {SC}. ServicePulse requires at least 6.6.0, and features switch on at 5.2, 5.4, 5.9, 6.6 and 6.19.ServicePulse reads
Total-Countfor counts and paging. It reads it onerrors,recoverability/groups/{id}/errors,customchecks,eventlogitems,redirectsandmessages2. It reads noLinkorETagheaders. The browser default HTTP cache still honoursETag.Many ids are derived from content, so they are the same on every member (
DeterministicGuid.MakeId, which is MD5 of the concatenated strings):MakeId(classifier, title)MakeId(fromPhysicalAddress)MakeId(name, hostId)MakeId(endpoint, hostId, checkId)MakeId(messageId, endpoint)FailureGroup/{groupId},"All"instance_idonbody_url)localhost:333331, 2, 3, …Commands on unknown ids are not safe to broadcast.
ArchiveMessageHandlerdereferencesGetFailedMessage(...)without a null check, and the EF Core store returnsFailedMessage?. An archive for an id that a member does not hold therefore fails in that member's message handler. Commands addressed by id must go to the owner (§5.2).The existing aggregation is only exact for page 1.
ScatterGatherApiasks every instance for the samepageandper_page, unions the results, re-sorts them and keepsper_pagerows.Total-Countovercounts after duplicates are removed.Ids appear inside payloads, not only in URLs. Examples:
edit_of, event logrelated_to: ["/message/{id}"](ServicePulse takessubstring(9)),headerssuch asServiceControl.Retry.UniqueMessageId, conversation messages, and Monitoring instance ids passed asclassifierFiltervalues torecoverability/groups/Endpoint Instance. Rewriting ids would mean rewriting all of these consistently.3. Architecture
Hosting. Inside a primary instance, enabled by configuration (§14).
/federation/api/and/federation/monitoring/. The primary's own/api/is unchanged, because the federation calls members'/api/, including this instance's when it is a member.Statelessness. The proxy keeps no durable state. It holds three kinds of in-memory cache, all rebuildable:
Running several replicas is safe.
No database and no transport. The proxy speaks HTTP only. It never sends NServiceBus messages; members do that when the proxy forwards commands.
3.1 Configuration
Two settings on the hosting primary, read like every other setting (environment variable, then
App.config, then registry):ServiceControl/EnableFederationSERVICECONTROL_ENABLEFEDERATIONtrueto host the federated API (defaultfalse)ServiceControl/FederatedInstancesSERVICECONTROL_FEDERATEDINSTANCESRemoteInstances[ { "api_uri": "http://sc-eu:33333/api/", "monitoring_uri": "http://mon-eu:33633/", "key": "eu", "name": "Europe" }, { "api_uri": "http://sc-us:33333/api/", "monitoring_uri": "http://mon-us:33633/", "key": "us", "name": "Americas" } ]api_uriis required. The first member is the lead member.keyis short and stable. It appears in the synthesisedinstance_idvalues (§5.3), so changing it breaks links ServicePulse has cached. When it is omitted, it is derived from the host and port ofapi_uri, for examplesc-us-33333.MemberTimeout4 s,DeepPagingMaxRows1000) and are not configurable yet.3.2 Choosing between typed contracts and JSON trees
Recommendation: handle payloads as
System.Text.Json.Nodes.JsonNodeand read only the fields needed to merge, sort or route (for exampleid,time_of_failure,count).ServiceControl.Persistencewould silently drop fields that newer members add.Unknown fields pass through untouched. The few fields the proxy computes, such as group
countandoperation_*, are written back into the node.4. Request handling model
Every proxied route is assigned exactly one strategy. The route table is explicit. A request for an unknown route goes to the lead member and is logged at warning level, so new ServiceControl routes keep working without silently returning wrong aggregates. A test (§12) catches routes that have no assigned strategy.
Total-Count)HEAD errors,heartbeats/stats,errors/summary, disconnected countmessages2, search, conversation, endpoint messageserrors/last/{id},errors/{id},groups/id/{id},sagas/{id}connection,edit/config, licensing report, auth configurationmy/routes,OPTIONS5. Identity and routing
5.1 Ids are passed through unchanged
Decision: the proxy does not rewrite resource ids. The one exception is
instance_id, covered in §5.3.Rewriting is attractive because a prefix such as
eu~<guid>would make every id unique and routing stateless. It was rejected for three reasons:related_to.substring(9).The cost of passing ids through is that the proxy must work out who owns an id (§5.2), and that a rare real collision of failed-message ids is acted on in both installations (§13).
5.2 Owner resolution
Commands addressed by id (
errors/retry [ids],errors/archive [ids],errors/unarchive [ids],edit/{id},pendingretries/retry [ids],pendingretries/resolve {uniquemessageids},customchecks/{id},endpoints/{id},redirects/{id}) must reach only the owner, because members do not tolerate unknown ids (§2, fact 8). Resolution has two steps:Owner cache. Every list or detail response the proxy relays records
id → memberfor the rows it contains. This covers failed messages, custom checks, endpoints and redirects. The cache is bounded (LRU), expires entries after a TTL, and can be rebuilt at any time.Probe on a miss. The proxy issues a cheap existence check to every member in parallel:
GET errors/last/{id}, 200 or 404GET redirectsThe command is then forwarded only to the members that answered 200.
Batch bodies, which are arrays of ids, are partitioned by owner. One request goes to each owning member with only its ids. The response is 202 if every partition was accepted. Ids with no owner are dropped and logged.
5.3 Synthesised
instance_idbody_urlcarries?instance_id=<memberInstanceId>. That value is a base64 URL and is not unique across members. For each message row in W/F results the proxy rewrites it to:body_urlhas noinstance_id(the member's local body store), the proxy appendsinstance_id={memberKey}..GET messages/{id}/body?instance_id=eu.abcthe proxy:.;euwithinstance_id=abc, or with noinstance_idifabcis empty;HttpForwarder, unchanged, keepingContent-Type.POST errors/{id}/retry?instance_id=;X-Particular-Incomplete-Resultsthat members return;instance_idfield on message rows, if present.Base64url never contains
., so splitting on the first.is unambiguous.When ServicePulse falls back to
messages/{message_id}/bodywith noinstance_id(saga diagram), the proxy uses strategy F: it asks members in turn and returns the first 200.6. Read aggregation details
6.1 Fan-out engine
The engine is modelled on
ScatterGatherApi, with these changes:Parallel requests. One
HttpClientper member, sent in parallel, each with its own timeout (default 4 s, so it fits inside ServicePulse's 5 s poll) and cancelled if the caller goes away.Error classification is the same as
ScatterGatherApi.FetchAndParse:errortimeoutunavailable, and the member is marked temporarily downMember health. A member that is marked down is skipped until a background probe (
GET {member}every 15 s) succeeds. This saves timeouts on every 5 s poll.Every aggregated response adds
X-Particular-Incomplete-Resultswith{memberKey}:{reason}entries for the failed members.messages2today. On other routes it is harmless and useful in diagnostics.If no member answers and at least one timed out, the proxy returns 504 with a problem+json body, as the primary does. If all members are unavailable it returns 503.
Authorization header. The caller's
Authorizationheader is forwarded to every member (§8).6.2 Union (U), sum (S) and key merge (K): field rules
GET endpointsid.GET endpoints/knownid.Total-Countis the row count.GET endpointssettingsnametrack_instancesfrom the lead member wins on conflict. The default entryname: ""comes from the lead member.GET heartbeats/statsactiveandfailing.GET errors/summaryEndpoints,HostsandMessage types, sum the counts per name.HEAD errors,HEAD recoverability/groups/{id}/errorsTotal-Countis the sum. The ETag is combined (§7).GET errors/queues/addressesphysical_addressfailed_message_count. Sort by count descending (the member sort order). Page in the proxy.GET recoverability/classifiersGET recoverability/groups/{classifier}idclassifierFilterquery parameter is forwarded.GET errors/groups/{classifier}(archived)idcount, take the minimumfirstand maximumlast; forcomment, see §6.3.GET recoverability/groups/id/{id},GET archive/groups/id/{id}GET recoverability/historyhistoric_operationsand sort bycompletion_timedescending. Keep the depth limit of a single member. Concatenateunacknowledged_operations.GET endpoints/{endpoint}/audit-countcountperutc_date.GET configuration/remotesapi_uri. Also add each non-lead member primary as an entry:{api_uri, version, status, configuration}from itsGET configuration. ServicePulse already modelsremote-errorinstances in platform health (PlatformModelStore.ts), so members appear there without UI changes.6.3 Merging failure groups
Groups are the most important merge, because the group id is the same on every member by design. For each group id, combine every member's
GroupOperation:title,type: taken from any member; they are identical.count: sum.first: minimum.last: maximum.comment: if all non-empty comments are equal, use that value. Otherwise use the lead member's comment, or the first non-empty one, and log the conflict. Writes are broadcast (§6.5), so comments converge after the next edit.Operation state. Each member reports its own state for the group.
operation_statusPreparing/ArchiveStarted>Forwarding/ArchiveProgressing>Waiting>Completed/ArchiveCompleted>None. A group is complete only when every participating member is complete.operation_progressoperation_remaining_count+operation_messages_completed_count)operation_remaining_count,operation_messages_completed_countoperation_failedneed_user_acknowledgementoperation_start_timeoperation_completion_time,last_operation_completion_timePseudo-groups that a member adds for unacknowledged completed operations use the same group id, so the same rules apply.
Sort by
lastdescending, which is the member order.Limit on group lists. Each member returns at most 200 groups (PR #5899). The merged list is cut at 200 after sorting, to keep the contract.
6.4 Sorted page merge (P)
This strategy applies to:
GET errorsandGET endpoints/{name}/errorsGET recoverability/groups/{id}/errorsGET customchecksGET eventlogitemsGET redirectsThe request carries
page,per_page,sortanddirection, plus filters such asstatus,modifiedandqueueaddress.Exact algorithm. The rows for page
pof sizesare rows(p-1)·s … p·s-1of the global order. Each member contributes at mostp·sof them. So:page=1&per_page=p·swith the same sort and filters.(p-1)·srows and takes.Total-Countto the sum of the members'Total-Countvalues.The merge is exact and stateless. Its cost grows linearly with page depth, so the proxy limits it with
DeepPagingMaxRows(default 1000 rows per member).What happens beyond the limit:
page=p&per_page=sunchanged, union, sort, takes.X-Particular-Incomplete-Results: proxy:approximate-pagingso the approximation is visible.ServicePulse usage makes deep pages rare:
Comparator. The comparator must reproduce each member's sort for the sort keys ServicePulse sends.
time_of_failure,message_type,time_of_retry_request,modified/last_modifiedidraised_atdescendingid(string)status, thenreported_at)idsortanddirectionquery parameters (from_physical_address,to_physical_address,last_modified)message_redirect_idSmaller choices:
Linkis rebuilt from the merged totals, like the primary'sWithPagingLinksbut usingceil(sum/s). ServicePulse ignoresLink, but ServiceInsight-style clients do not.idon event log rows to{memberKey}.{id}for those rows. This is the one id rewrite besidesinstance_id, and it is safe because no API accepts an event log id.6.5 Message searches and conversations (W)
GET messages2pages with a time window andpage_size, and has no offset. Forward the request unchanged, merge by the requested sort, and takepage_size. This is exact.The other searches:
GET messagesGET messages/search,GET messages/search/{keyword}GET endpoints/{e}/messages,GET endpoints/{e}/messages/search[/{keyword}]GET conversations/{id}These are scatter-gather endpoints on each member already. The proxy applies P to the ones that are paged and plain union to
conversations/{id}, which returns the whole conversation. It also de-duplicates rows byreceiving_endpoint.name + message_id, asScatterGatherApiMessageViewdoes, giving priority to the lead member.ServicePulse uses these as follows:
messages/search/{message_id}is used to load a message's details. The UI picks the row whoseidmatches, so merging all hits is correct.messages/search?q={sagaId}is used to find saga message bodies.GET sagas/{id}: if one member answers, return its result. If several answer, merge thechangesarrays ordered byfinish_timedescending, asGetSagaByIdApidoes today.6.6 Root document and configuration
GET {SC}(X). The proxy synthesisesRootUrlswith its own base URL. It buildsmy_routes_url,errors_urland the other URLs from the incoming request, respectingX-Forwarded-*. Field values:name: a configuredDisplayName, defaulting to "ServiceControl (federated)".license_status: the worst status across members.X-Particular-Version: the lowest member version, so ServicePulse never enables a feature that one member lacks.platform_health_version: the version of the member that answered, normally the lead. ServicePulse shows it in platform health in preference toX-Particular-Version, but its feature gates read only the header, so the header keeps the lowest version. The other member primaries have their own rows inconfiguration/remotes.GET configuration(andGET instance-info). The lead member's document, with these fields changed:data_retention.error_retention_periodmass_transit_connectorerror_queuesandlogs. Its presence hides the flow, sequence and saga tabs in ServicePulse, so this needs a product decision (§13).GET connection(L). Answered by the lead member.ErrorQueue,ErrorQueue1, …), reusing the duplicate-key convention ofPlatformConnectionDetails. This produces a confusing snippet, so it is not recommended.7. Headers and HTTP caching
Total-CountLinkETagW/"…"built fromDataVersion.Combine-style hashing of(memberKey, memberETag)pairs, sorted by member key. If any member returned no ETag, or failed, no ETag is emitted (the "absence never answers 304" rule indocs/data-versioning-design.md). For P routes the member ETags cover differentper_pagewindows than the client asked for; that is still a sound validator, because a member's window ETag moves whenever its rows within the window move.If-None-Match(memberKey, upstream URL) → ETag + bodyper URL in a small memory cache and sendsIf-None-Matchupstream. A member 304 lets it reuse the cached body. This cuts member load, since ServicePulse polls the same URLs every 5 s.X-Particular-VersionX-Particular-Incomplete-ResultsX-Particular-ReasonContent-Type,Content-DispositionCache-Control: private, max-age=0,Vary: AcceptETag, Last-Modified, Link, Total-Count, X-Particular-Version, Content-Disposition, Request-Id, X-Particular-Incomplete-Results, plusAllowon the Monitoring surface8. Authentication and authorization
{SC}and{MU}. The proxy forwards theAuthorizationheader unchanged to members.OpenIdConnectSettingsshape as ServiceControl (ServiceControl.Hosting/Auth).GET authentication/configuration(anonymous, strategy L). Answered by the lead member. At startup the proxy checks that every member returns the sameenabled,authorityandaudience, and logs an error if not.GET my/routes(X). The proxy calls each member'smy/routeswith the caller's token and returns:/api/...templates, which is the shape ServicePulse normalises inrouteMatching.ts.GET api/my/routes. Same approach over the member Monitoring instances.configuration,instance-info,configuration/remotesandauthentication/configuration.9. Commands (writes)
All member commands are asynchronous: they return 202 and send a local NServiceBus message. Progress shows up later in polled reads.
9.1 Status-code merging for broadcast writes
X-Particular-ReasonX-Particular-Incomplete-Results9.2 Command routing table
POST errors/retry[ids]POST errors/{id}/retry[?instance_id]instance_idif present (§5.3), else from the owner resolverPATCH/POST errors/archive[ids],PATCH errors/unarchive[ids]errors/archive/is accepted too.PATCH/POST errors/{id}/archivePATCH errors/{from}...{to}/unarchivePOST errors/retry/allPOST errors/{endpoint}/retry/allPOST errors/queues/{queue}/retryPOST edit/{id}POST pendingretries/retry[ids],PATCH pendingretries/resolve {uniquemessageids}POST pendingretries/retry {from,to},PATCH pendingretries/resolve {from,to}, `.../queues/retryPOST/DELETE recoverability/groups/{id}/commentDELETE recoverability/unacknowledgedgroups/{id}DELETE customchecks/{id}PATCH endpoints/{id}{monitor_heartbeat},DELETE endpoints/{id}PATCH endpointssettings/{name?}nameempty) applies everywherePOST/PUT/DELETE redirects[...]POST notifications/email,.../toggleGET notifications/emailcomes from the lead member.authentication_passwordis returned in clear by members; the proxy passes it through unchanged and does not log bodies.POST notifications/email/testX-Particular-Reasonnames the memberPOST license/detailsUploadPOST licensing/endpoints/update,POST licensing/settings/masks/updatePOST maintenance/retention/purgelast_outcomeis the worst9.3 Redirects
A redirect maps a failed queue address to another address on one member's broker.
from_physical_address.errors/queues/addresseslist contains the source queue. Strategy R then resolves the owner byfrom_physical_address, andGET redirectsunions the members' lists.10. Monitoring surface (
{MU})GET /platform_health_versionset to its own version andplatform_health_statusset to the worst health across members (at leastdegradedwhen one does not answer).X-Particular-Versionis the lowest member Monitoring version. ServicePulse has one Monitoring row and no remote Monitoring role, so other members' Monitoring instances have no row of their own.OPTIONS /Allowis the intersection of the members'Allowvalues (so it advertisesDELETEonly if all members support it)GET monitored-endpoints?history=NName(default)GET monitored-endpoints/{name}?history=NInstances: concatenate.MessageTypes: merge byId(type identity), combining their metrics.Digest: recompute from the merged series.MetricDetails.Metrics: merge index by index withTimeAxisValuesfrom the lead member.GET monitored-endpoints/disconnectedhistory=1fan-out.DELETE monitored-instance/{name}/{instanceId}EndpointInstanceIdsper member)GET connection{ Metrics: { Enabled, MetricsQueue, Interval } }from the lead memberGET licenseGET api/my/routesMerging
monitored-endpointsby name (recommended).historyperiod. Each series has 60 buckets aligned to wall-clock UTC, so points can be combined index by index.Throughput,RetriesandQueueLength: sum.ProcessingTimeandCriticalTime: average weighted by the members'Throughputpoints at the same index. Use a plain average when throughput is 0.IsStale: AND (the merged endpoint is stale only if every member says so).ConnectedCountandDisconnectedCount: sum.EndpointInstanceIds: concatenate.recoverability/groups/Endpoint Name?classifierFilter={name}, and the proxy merges those groups by id (§6.3). Merging monitored endpoints by name is therefore consistent with the failed-message counts shown beside them.MergeMonitoredEndpointsByName=falsesuffixes names as{name} ({memberKey})and strips the suffix onmonitored-endpoints/{name}. This breaks the group lookup above, so it is not the default.historymust be one of 1, 5, 10, 15, 30 or 60. Members throw 500 on other values, so the proxy validates up front and returns 400.10.1 Load profile
The proxy multiplies member load by roughly N.
On every page, every 5 s, each ServicePulse tab triggers about ten calls:
configuration,configuration/remotescustomchecks?status=failerrors?status=unresolved(count only)endpoints,endpointssettingsmonitored-endpoints/disconnectedeventlogitems(dashboard)Mitigations:
11. Licensing and usage
GET license?refresh&clientNamestatus/license_statusis the worst across members;instance_nameis the proxy display nameGET license/detailsGET licensing/endpointsname_hash + scope_hash:max_daily_throughputis the sum of daily maxima. That is an over-estimate, but members do not expose daily series.monthly_throughputis summed per month.GET licensing/settings/info,.../settings/test,.../maskssettings/testshould be shown as healthy only if every member is healthy. Its result fields can be ANDed per connection type.GET licensing/report/availablereport_can_be_generated= AND across members;reasonfrom the first member that cannotGET licensing/report/file?spVersion=ThroughputCollector→SignedReport) and cannot be merged. The proxy downloads each member's report zip and returns one outer zip,{customer}.throughput-reports-{timestamp}.zip, holding one entry per member ({memberKey}-…zip), withContent-Disposition. Whoever consumes the report must accept a bundle; to confirm with the licensing team.12. Implementation outline
This section describes structure only; no code yet.
Route table as data. Each SC route is declared once with its strategy and parameters (merge key, comparator, owner extractor). That keeps the mapping in §§6–11 reviewable in one file.
Forwarding. Streamed pass-through cases (bodies, routed commands, lead-member reads) use YARP
IHttpForwarder, as the primary does for its remotes.Code reuse from ServiceControl.
DataVersion(ServiceControl.Infrastructure)ServiceControl.Hosting(forwarded headers, auth, request id)OpenIdConnectSettingsThe proxy does not reference persistence or the NServiceBus packages.
Tests
HttpApiRoutes.approved.txtfiles plus the licensing routes, and assert that every route has a strategy in the proxy route table. Adding a route to ServiceControl then fails the proxy build until it is classified.ServiceControl.MultiInstance.AcceptanceTestsstyle to run two primaries and the proxy in process, and drive the ServicePulse-visible flows: list, retry, archive group, edit and retry, body fetch.src/Frontend/test/preconditions,test/mocks/service-control-instance-template.ts) against proxy-produced responses to catch shape drift.13. Risks and open questions
platform_health_statusandplatform_health_versionfrom the root documents of{SC}and{MU}, and callsPOST license/amendDetails. Neither exists in this branch of ServiceControl. The proxy has to support them once the members do. Platform health is handled as described in §6.6 and §10;license/amendDetailsgoes to the lead member.Total-Countcounts both, and commands would reach both owners.mass_transit_connectorhides UI features. One MassTransit-enabled member would hide the flow and saga tabs for everyone. A product decision is needed.configuration/remotes.O(page·per_page)per member; it is bounded byDeepPagingMaxRowsand then approximate. An alternative is keyset paging, which would need member API changes and is out of scope.AGENTS.md, it is a candidate for an ADR underdocs/decisions/once the design is agreed. This draft is a working document, not that record.14. Initial implementation
The code is in the
src/ServiceControl.Federationlibrary, with tests insrc/ServiceControl.Federation.UnitTests. Both projects are inServiceControl.slnxunder/Instances/Federation/.The primary instance references the library, so
ServiceControl.Federation.dllships in the primary's deploy folder and therefore in theservicecontrolcontainer image. No Dockerfile changes are needed.Hosting in the primary
Settings.csEnableFederationandFederatedInstances, and validates them at startup when federation is enabledRunCommand.csAddServiceControlFederationandMapServiceControlFederationwhen enabled, next to the integrated ServicePulse/federation/api/, and with/federation/monitoring/if any member has a Monitoring URL./api/stays the instance's own API.Federation:line with the number of membersLayout
FederationHostingExtensions.csAddServiceControlFederation(FederationSettings)andMapServiceControlFederation(), the two calls the host makesConfiguration/FederationSettings.csFederatedInstancesJSON (§3.1), and validationMembers/MemberGateway(HTTP calls and failure classification),MemberHealthMonitor(background probe)Aggregation/SortedPageMerger(§6.4),RowOrder(comparators that mirror each persister's sort),FailureGroupMerger(§6.3),MonitoredEndpointMerger(§10),CommandOutcome(§9.1),VersionsRouting/InstanceIdCodec(§5.3) andOwnerCache(§5.2)Http/Api/Federator.csApi/PrimaryRoutes.cs,Api/MonitoringRoutes.csApi/*Handlers.csHow it differs from the design
X-Particular-Incomplete-Resultsaserror, as well as members that time out or are unreachable.~as the separator. Event log ids become{memberKey}~{id}, andinstance_idbecomes{memberKey}~{original}..is used by ServiceControl's base64url padding, so it cannot be the separator.DELETE monitored-instance/...is broadcast rather than routed. Removal only drops the instance from a member's in-memory registry, which is harmless on members that do not know it.Not implemented yet
/connectionfrom several members (currently the lead member answers)Tests
instance_idencoding, version selection.instance_id, group merge, and an unreachable member.APIApprovals.PlatformSampleSettingsincludes the two new settings.Running it
On any primary, for example in the
servicecontrolcontainer:Then open the primary's root URL. The embedded ServicePulse shows the federated view.
?scu=http://<primary>:33333/federation/api/&mu=http://<primary>:33333/federation/monitoring/.localhostis the container itself. That is correct for the hosting instance; other members need addresses the container can reach./apiwas unchanged. The embedded ServicePulse's configuration pointed at/federation/..., and the federated root, failed-message list and remotes answered. A member's stopped Monitoring instance was reported inX-Particular-Incomplete-Results.