The OSGi framework provides a local service registry for bundles to communicate through service objects, where a service is an object that one bundle registers and another bundle gets. A distribution provider can use this loose coupling between bundles to export a registered service by creating an endpoint. Vice versa, the distribution provider can create a proxy that accesses an endpoint and then registers this proxy as an imported service. A Framework can contain multiple distribution providers simultaneously, each independently importing and exporting services.
An endpoint is a communications access mechanisms to a service in another framework, a (web) service, another process, or a queue or topic destination, etc., requiring some protocol for communications. The constellation of the mapping between services and endpoints as well as their communication characteristics is called the topology. A common case for distribution providers is to be present on multiple frameworks importing and exporting services; effectively distributing the service registry.
The local architecture for remote services is depicted in Figure 100.1 on page .
Local services imply in-VM call semantics. Many of these semantics cannot be supported over a communications connection, or require special configuration of the communications connection. It is therefore necessary to define a mechanism for bundles to convey their assumptions and requirements to the distribution provider. This chapter defines a number of service properties that a distribution provider can use to establish a topology while adhering to the given constraints.
General abstractions for distributed systems have been tried before and often failed. Well known are the fallacies described in [1] The Fallacies of Distributed Computing Explained:
-
The network is reliable
-
Latency is zero
-
Bandwidth is infinite
-
The network is secure
-
Topology doesn't change
-
There is one administrator
-
Transport cost is zero
-
The network is homogeneous
Most fallacies represent non-functional trade-offs that should be considered by administrators, their decisions can then be reflected in the topology. For example, in certain cases limited bandwidth is acceptable and the latency in a datacenter is near zero. However, the reliability fallacy is the hardest because it intrudes into the application code. If a communication channel is lost, the application code needs to take specific actions to recover from this failure.
This reliability aspect is also addressed with OSGi services because services are dynamic. Failures in the communications layer can be mapped to the unregistration of the imported service. OSGi bundles are already well aware of these dynamics, and a number of programming models have been developed to minimize the complexity of writing these dynamic applications.
This section introduces a number of properties that participating bundles can use to convey information to the distribution provider according to this Remote Service specification.
The following table lists the properties that must be listed by a distribution provider.
Table 100.1 Remote Service Properties registered by the Distribution Provider
Service Property Name | Type | Description |
---|---|---|
remote.configs.supported |
String+ |
Registered by the distribution provider on one of its services to indicate the supported configuration types. See Configuration Types and Dependencies. |
remote.intents.supported |
String+ |
Registered by the distribution provider on one of its services to indicate the vocabulary of implemented intents. See Dependencies. |
service.imported |
* |
Must be set by a distribution provider to any value when it registers the endpoint proxy as an imported service. A bundle can use this property to filter out imported services. |
service.imported.configs |
String+ |
The configuration information used to import this
service, as described in If multiple configuration types are listed in this property, then they must be synonyms for exactly the same remote endpoint that is used to export this service. |
service.intents |
String+ |
A distribution provider must use this property to convey the combined intents of:
|
The properties for bundles providing services to be exported or require services to be imported are listed alphabetically in the following table. The scenarios that these properties are used in are discussed in later sections.
Table 100.2 Remote Service Properties registered by Exporting bundles
Service Property Name | Type | Description |
---|---|---|
service.exported.configs |
String+ |
A list of configuration types that should be used to export the service. Each configuration type represents the configuration parameters for one or more Endpoints. A distribution provider should create endpoints for each configuration type that it supports. See Configuration Types for more details. If this property is not set or empty a distribution provider is free to choose a default configuration type for the service. |
service.exported.intents |
String+ |
A list of intents that the distribution provider must implement to distribute the service. Intents listed in this property are reserved for intents that are critical for the code to function correctly, for example, ordering of messages. These intents should not be configurable. For more information about intents, see Intents. This property is optional. |
service.exported.intents.extra |
String+ |
This property is merged with the
|
service.exported.interfaces |
String+ |
Setting this property marks this service for export.
It defines the interfaces under which this service can be
exported. This list must be a subset of the types listed in the
|
service.intents |
String+ |
A list of intents that this service implements. A distribution provider must use this property to convey the combined intents of:
To export a service, a distribution provider must expand any qualified intents to include those supported by the endpoint. This can be a subset of all known qualified intents. See Intents. This property is optional for registering bundles. |
service.pid |
String+ |
Services that are exported should have a
|
The properties and their treatment by the distribution provider is depicted in Figure 100.2.
A distribution provider should create one or more endpoints for an exported service when the following conditions are met:
-
The service has the service property
service.exported.interfaces
set. -
All intents listed in
service.exported.intents
,service.exported.intents.extra
andservice.intents
are part of the distributed provider's vocabulary -
None of the intents are mutually exclusive.
-
The distribution provider can use the configuration types in
service.exported.configs
to create one or more endpoints.
The endpoint must at least implement all the intents that are
listed in the service.exported.intents
and
service.exported.intents.extra
properties.
The configuration types listed in the
service.exported.configs
can contain
alternatives and/or synonyms.
Alternatives describe different endpoints for the same service while a
synonym describes a different configuration type for the same
endpoint.
A distribution provider should create endpoints for each of the configuration types it supports; these configuration types should be alternatives. Synonyms are allowed.
If no configuration types are recognized, the distribution
provider should create an endpoint with a default configuration type
except when one of the listed configuration types is
<<nodefault>>
.
For more information about the configuration types, see further Configuration Types.
An imported service must be a normal service, there are therefore no special rules for getting it. An imported service has a number of additional properties that must be set by the distribution provider.
If the endpoint for an exported service is imported as an OSGi service in another framework, then the following properties must be treated as special.
-
service.imported
- Must be set to some value. -
service.intents
- This must be the combination of the following:-
The
service.intents
property on the exported service -
The
service.exported.intents
andservice.exported.intents.extra
properties on the exported service -
Any additional intents implemented by the distribution providers on both sides.
-
-
service.imported.configs
- Contains the configuration types that can be used to import this service. The types listed in this property must be synonymous, that is, they must refer to exactly the same endpoint that is exporting the service. See Configuration Types. -
service.exported.*
- Properties starting withservice.exported.
must not be set on the imported service. -
service.exported.interfaces
- This property must not be set, its content is reflected in theobjectClass
property.
All other public service properties (not
starting with a full stop ('.' \u002E
)) must be listed on
the imported service if they use the basic service property types. If
the service property cannot be communicated because, for example, it
uses a type that can not be marshaled by the distribution provider then
the distribution provider must ignore this property.
The service.imported
property indicates that a
service is an imported service. If this service property is set to any
value, then the imported service is a proxy for an endpoint. If a bundle
wants to filter out imported services, then it can add the following
filter:
(&(!(service.imported=*)) <previousfilter>)
Distribution providers can also use the Service Hook Service Specification of OSGi Core Release 8 to hide services from specific bundles.
The Service Hooks Service Specification of OSGi Core Release 8, allows a distribution provider to detect when a bundle is listening for specific services. Bundles can request imported services with specific intents by building an appropriate filter. The distribution provider can use this information to import a service on demand.
The following example creates a Service Tracker that is interested in an imported service.
Filter f = context.createFilter(
"(&(objectClasss=com.acme.Foo)"
+ "(service.intents=confidentiality))"
);
ServiceTracker tracker =
new ServiceTracker(context, f, null );
tracker.open();
Such a Service Tracker will inform the Listener Hook and will give
it the filter expression. If the distribution provider has registered
such a hook, it will be informed about the need for an imported
com.acme.Foo
service that has a
confidentiality
intent. It can then use some proprietary
means to find a service to import that matches the given object class
and intent.
How the distribution provider finds an appropriate endpoint is out of scope for this specification.
An intent is a name for an abstract distribution capability. An
intent can be implemented by a service; this can then
be reflected in the service.intents
property. An intent can
also constrain the possible communication mechanisms
that a distribution provider can choose to distribute a service. This is
reflected in the service.exported.intents
and
service.exported.intents.extra
properties.
The purpose of the intents is to have a vocabulary that is shared between distribution aware bundles and the distribution provider. This vocabulary allows the bundles to express constraints on the export of their services as well as providing information on what intents are implemented by a service.
Intents have the following syntax
intent ::= token ( '.' token )?
Qualified intents use a full stop ('.'
\u002E
) to separate the intent from the qualifier. A qualifier
provides additional details, however, it implies its prefix. For
example:
confidentiality.message
This example, can be expanded into
confidentiality
and confidentiality.message
.
Qualified intents can be used to provide additional details how an intent
is achieved. However, a Distribution Provider must expand any qualified
intents to include those supported by the endpoint. This can be a subset
of all known qualified intents.
The concept of intents is derived from the [3] SCA Policy Framework specification. When designing a vocabulary for a distribution provider it is recommended to closely follow the vocabulary of intents defined in the SCA Policy Framework.
Remote Services implementations have a large amount of freedom. For example, they may use any mechanism that they choose to transmit data between the caller of the remote service and the provider of the service. This freedom means that there can be a large variation in the behaviors supported by different Remote Services implementations.
The purpose of the osgi.basic
intent
is to provide a common set of rules that can be relied upon when
exporting a simple remote service. This includes rules about the service
interface, including supported parameter and return types, as well as a
means of configuring a timeout for remote invocations.
Remote Services implementations which offer the
osgi.basic
intent must support remote services which
advertise a single Java interface containing zero or more
methods.
The following types must be supported as declared parameters or returns from methods on the remote service:
-
Primitive values
-
The OSGi scalar types, OSGi Version objects, Java enums, and types which conform to the OSGi DTO rules as described in the OSGi core specification. In the rest of this section these will be known as the basic types.
-
Arrays of primitive values or the basic types
-
Lists, Collections or Iterables of the basic types, however the implementation of the collection may not be preserved in transit. For example a
LinkedList
may be converted to anArrayList
. -
Sets of the OSGi basic types where
equals
is used to determine identity.SortedSet
is not required to be supported due to the difficulties associated with serializing comparators. The implementation of the set may not be preserved in transit. For example aLinkedHashSet
may be converted to aHashSet
. -
Maps where the keys and values are the OSGi basic types, and
equals
is used to determine identity for the keys.SortedMap
is not required to be supported due to the difficulties associated with serializing comparators. The implementation of the map may not be preserved in transit. For example aLinkedHashMap
may be converted to aHashMap
. -
Methods with no arguments, and methods with a
void
return
The implementation of a Remote Services provider is entirely opaque. In many cases there will be no feedback mechanism if the remote call hangs, or if the remote node fails. The local client must therefore decide at what point to fail after a certain amount of time has elapsed.
A single Remote Services implementation must be able to handle a wide variety of different remote service invocations across many services, therefore it is difficult to identify a sensible timeout for the remote service invocation. Some calls may be quick, and so a ten second timeout is desirable for rapid failure detection, other calls may be long-running, and a two minute timeout too short. The remote service must therefore be able to declare its own timeout.
To declare a timeout the remoteable service may provide a
service property osgi.basic.timeout
which provides a
timeout value in milliseconds. The value may be declared as a
String
or as a Number
, which will be
converted into a Long. The timeout value is used to limit the maximum
time for which a remote service client will be blocked waiting for a
response. The same timeout value applies to all methods on the
service. In the event that the invocation reaches the timeout value
the client must fail the method call with a ServiceException with its
type set to REMOTE.
Some service invocations operate asynchronously, returning quickly and continuing to process in the background. For void methods with no completion notifications this is simple to achieve remotely, but more useful scenarios are difficult to support without using higher-level abstractions to represent the eventual result.
The purpose of the osgi.async
intent is to provide a
common set of rules that can be relied upon for remote services which
return types representing an asynchronously executing method.
The osgi.async
intent makes no guarantees about the
service interface(s) or method parameters supported by the remote
services implementation. It is therefore recommended that it be used in
conjunction with another intent, such as the osgi.basic intent.
Asynchronous returns are implemented using a holder type. The holder represents the state of the asynchronous execution, and can be queried for its completion state. When the execution is complete the holder can be queried for the result of the execution, or for its failure.
The following holder types must be supported as return types from methods on the remote service:
-
org.osgi.util.promise.Promise
-
java.util.concurrent.Future
-
java.util.concurrent.CompletionStage
-
java.util.concurrent.CompletableFuture
The full set of supported types for the eventual return value
encapsulated by the holder object are not defined by the
osgi.async
intent. Instead the full set of supported
types can be inferred from the other supported intents supported by
the Remote Services implementation. For example the osgi.basic intent would ensure
support for a return value of
Promise<List<String>>
If an asynchronous remote execution fails then the holder type must be failed with the same exception that would have been thrown in a synchronous call.
The reason for the failure may be as a result of a failure in communications, a timeout, or because the remote invocation resulted in an exception
The osgi.confidential
intent can be used to state
that the remote service communications must only be readable by the
intended recipient, for example, through the use of TLS-based transport
encryption.
If a Remote Services implementation does not support confidential communications, or is not configured as such, it must not expose the service remotely.
In many deployment scenarios, including cloud, embedded or IoT
deployments, hosts may be accessible via a public network and via a
private network. In such cases hosts will have multiple IP addresses to
separate public network access from private network access. Private IP
addresses normally in one of the following blocks:
10.0.0.0/8
, 172.16.0.0/12
or
192.168.0.0/16
.
In many cases it is desirable to expose remote services only on
the private network so that these services cannot be accessed from the
outside world. This is especially useful if this service is used as a
microservice within a larger application. The osgi.private
intent can be specified for this purpose.
If the osgi.private
intent is required on the remote
service, it will only be exposed as a remote service on a private
network on the host. If the host does not support a private IP address
or if the Remote Services implementation does not have the information
to decide whether a host IP is private, the service should not be
exposed.
Normal service semantics are call-by-reference. An object passed as an argument in a service call is a direct reference to that object. Any changes to this object will be shared on both sides of the service registry.
Distributed services are different. Arguments are normally passed by value, which means that a copy is sent to the remote system, changes to this value are not reflected in the originating framework. When using distributed services, call-by-value should always be assumed by all participants in the distribution chain.
Services are syntactically defined by their Java interfaces. When exposing a service over a remote protocol, typically such an interface is mapped to a protocol-specific interface definition. For example, in CORBA the Java interfaces would be converted to a corresponding IDL definition. This mapping does not always result in a complete solution.
Therefore, for many practical distributed applications it will be necessary to constrain the possible usage of data types in service interfaces. A distribution provider must at least support interfaces (not classes) that only use the basic types as defined for the service properties. These are the primitive types and their wrappers as well as arrays and collections. See Filter Syntax of OSGi Core Release 8 for a list of service property types.
Distribution providers will in general provide a richer set of types that can be distributed.
A distributed service must closely track any modifications on the corresponding service registration. If service properties are modified, these modifications should be propagated to the distributed service and associated service proxies. If the exported service is unregistered, the endpoint must be withdrawn as soon as possible and any imported service proxies unregistered.
An imported service is just like any other service and can be used as such. However, certain non-functional characteristics of this service can differ significantly from what is normal for an in-VM object call. Many of these characteristics can be mapped to the normal service operations. That is, if the connection fails in any way, the service can be unregistered. According to the standard OSGi contract, this means that the users of that service must perform the appropriate cleanup to prevent stale references.
It is impossible to guarantee that a service is not used when it is no longer valid. Even with the synchronous callbacks from the Service Listeners, there is always a finite window where a service can be used while the underlying implementation has failed. In a distributed environment, this window can actually be quite large for an imported service.
Such failure situations must be exposed to the application code
that uses a failing imported service. In these occasions, the
distribution provider must notify the application by throwing a Service
Exception, or subclass thereof, with the reason REMOTE
. The
Service Exception is a Runtime Exception, it can be handled higher up in
the call chain. The cause of this Service Exception must be the
Exception that caused the problem.
A distribution provider should log any problems with the communications layer to the Log Service, if available.
An exported service can have a service.exported.configs
service property. This property lists configuration types for endpoints
that are provided for this service. Each type provides a specification
that defines how the configuration data for one or more endpoints is
provided. For example, a hypothetical configuration type could use a
service property to hold a URL for the RMI naming registry.
Configuration types that are not defined by the OSGi Working Group
should use a name that follows the reverse capabilities
domain name scheme defined in [4] Java Language Specification for Java
packages. For example, com.acme.wsdl
would be the proprietary
way for the ACME company to specify a WSDL configuration type.
The service.exported.configs
and
service.imported.configs
use the configuration types in
very different ways. That is, the service.imported.configs
property is not a copy of the service.exported.configs
as
the name might seem to imply.
An exporting service can list its desired
configuration types in the service.exported.configs
property. This property is potentially seen and interpreted by multiple
distribution providers. Each of these providers can independently create
endpoints from the configuration types. In principle, the
service.exported.configs
lists
alternatives for a single distribution provider and
can list synonyms to support alternative
distribution providers. If only one of the synonyms is useful, there is
an implicit assumption that when the service is exported, only one of
the synonyms should be supported by the installed distribution
providers. If it is detected that this assumption is violated, then an
error should be logged and the conflicting configuration is further
ignored.
The interplay of synonyms and alternatives is depicted in Table 100.3. In this table, the first
columns on the left list different combinations of the configuration
types in the service.exported.configs
property. The next
two columns list two distribution providers that each support an
overlapping set of configuration types. The x
's in this
table indicate if a configuration type or distribution provider is
active in a line. The description then outlines the issues, if any. It
is assumed in this table that hypothetical configuration types
net.rmi
and com.rmix
map to an identical
endpoint, just like net.soap
and
net.soapx
.
Table 100.3 Synonyms and Alternatives in Exported Configurations
service.exported. configs | Distribution Provider A | Distribution Provider B | Description | ||||
---|---|---|---|---|---|---|---|
net.rmi |
com.rmix |
net.soap |
com.soapx |
<<no default>> |
Supports:
|
Supports:
|
|
|
|
|
OK, A will create an endpoint for the RMI and SOAP alternatives. |
||||
|
|
|
Configuration error. There is
a clash for |
||||
|
|
|
OK, exported on com.soapx by A, the net.soap is ignored. |
||||
|
|
|
|
Synonym error because A and B export to same SOAP endpoint, it is likely that one will fail. |
|||
|
|
|
|
OK, two alternative endpoints over RMI (by A) and SOAP (by B) are created. This is a typical use case. |
|||
|
|
|
OK. Synonyms are used to allow frameworks that have either A or B installed. In this case A exports over SOAP. |
||||
|
|
|
OK. Synonyms are used to allow frameworks that have either A or B installed. In this case B exports. |
||||
|
OK. A creates an endpoint with default configuration type. |
||||||
|
|
OK. Both A and B each create an endpoint with their default configuration type. |
|||||
|
|
OK. No endpoint is created. |
|||||
x |
x |
x |
Provider B does not recognize the configuration types it should therefore use a default configuration type. |
To summarize, the following rules apply for a single distribution provider:
-
Only configuration types that are supported by this distribution provider must be used. All other configuration types must be ignored.
-
All of the supported configuration types must be alternatives, that is, they must map to different endpoints. Synonyms for the same distribution provider should be logged as errors.
-
If a configuration type results in an endpoint that is already in use, then an error should be logged. It is likely then that another distribution provider already had created that endpoint.
An export of a service can therefore result in multiple endpoints being created. For example, a service can be exported over RMI as well as SOAP. Creating an endpoint can fail, in that case the distribution provider must log this information in the Log Service, if available, and not export the service to that endpoint. Such a failure can, for example, occur when two configuration types are synonym and multiple distribution providers are installed that supporting this type.
On the importing side, the service.imported.configs
property lists configuration types that must refer to the same endpoint.
That is, it can list alternative configuration types for this endpoint
but all configuration types must result in the same endpoint.
For example, there are two distribution providers installed at the
exporting and importing frameworks. Distribution provider A supports the
hypothetical configuration type net.rmi
and
net.soap
. Distribution provider B supports the hypothetical
configuration type net.smart
. A service is registered that
list all three of those configuration types.
Distribution provider A will create two endpoints, one for RMI and
one for SOAP. Distribution provider B will create one endpoint for the
smart protocol. The distribution provider A knows how to create the
configuration data for the com.acme.rmi
configuration type
as well and can therefore create a synonymous description of the
endpoint in that configuration type. It will therefore set the imported
configuration type for the RMI endpoint to:
service.imported.configs = net.rmi, com.acme.rmi
net.rmi.url = rmi://172.25.25.109:1099/service-id/24
com.acme.rmi.address = 172.25.25.109
com.acme.rmi.port = 1099
com.acme.rmi.path = service-id/24
A bundle that uses a configuration type has an implicit dependency on the distribution provider. To make this dependency explicit, the distribution provider must register a service with the following properties:
-
remote.intents.supported
- (String+
) The vocabulary of the given distribution provider. -
remote.configs.supported
- (String+
) The configuration types that are implemented by the distribution provider.
A bundle that depends on the availability of specific intents or
configuration types can create a service dependency on an anonymous
service with the given properties. The following filter is an example of
depending on a hypothetical net.rmi
configuration
type:
(remote.configs.supported=net.rmi)
The distribution provider will be required to invoke methods on any exported service. This implies that it must have the combined set of permissions of all methods it can call. It also implies that the distribution provider is responsible for ensuring that a bundle that calls an imported service is not granted additional permissions through the fact that the distribution provider will call the exported service, not the original invoker.
The actual mechanism to ensure that bundles can get additional permissions through the distribution is out of scope for this specification. However, distribution providers should provide mechanisms to limit the set of available permissions for a remote invocation, preferably on a small granularity basis.
One possible means is to use the
getAccessControlContext
method on the Conditional Permission
Admin service to get an Access Control Context that is used in a
doPrivileged
block where the invocation takes place. The
getAccessControlContext
method takes a list of signers which
could represent the remote bundles that cause an invocation. How these are
authenticated is up to the distribution provider.
A distribution provider is a potential attack point for intruders. Great care should be taken to properly setup the permissions or topology in an environment that requires security.
Service registration and getting services is controlled through
the ServicePermission
class. This permission supports a
filter based constructor that can assert service properties. This
facility can be used to limit bundles from being able to register
exported services or get imported services if they are combined with
Conditional Permission Admin's ALLOW
facility. The
following example shows how all bundles except from
www.acme.com
are denied the registration and getting of
distributed services.
DENY {
[...BundleLocationCondition("http://www.acme.com/*" "!")]
(...ServicePermission "(service.imported=*)" "GET" )
(...ServicePermission "(service.exported.interfaces=*)"
"REGISTER" )
}
[1]The Fallacies of Distributed Computing Explainedhttps://www.researchgate.net/publication/322500050_Fallacies_of_Distributed_Computing_Explained
[2]Service Component Architecture (SCA)https://www.osoa.org/
[3]SCA Policy Framework specificationhttps://www.osoa.org/
[4]Java Language Specificationhttps://docs.oracle.com/javase/specs/