...
At runtime the Ignite user installs all required bundles (including ignite-core, any optional Ignite bundles as well as the application bundles) using either the standard mechanisms defined by the OSGi spec, or relying on the container's implementation-specific capabilities. For example, Apache Karaf (an OSGi implementation) offers a packaging/deloyment concept called "Feature" which roughly speaking is a list of bundles to automatically deploy when the OSGi Framework starts.
The main problem we need to solve in order to allow Ignite OSGi enablement is the marshalling. More specifically the issue is with deserialization of the classes that are provided by the bundles other than the JDK and the Ignite bundle itself.
[raul.kripalani]: I've already modified the POMs to generate and package the MANIFEST.MF with the appropriate OSGi headers. The code is pushed to the ignite-1527 branch.
We should provide a Feature Repository to make it easier for Apache Karaf users to install Ignite and, optionally, Ignite modules. There should be one feature per module that also installs all necessary library dependencies.
The main problem we need to solve in order to allow Ignite OSGi enablement is the marshalling. More specifically the issue is with deserialization of the classes that are provided by the bundles other than the JDK and the Ignite bundle itself.
When the Ignite transport layer receives a message it needs to figure out how to deserialize the bytes and for that it needs to know the bundle that provides the class to be deserialized. To make things more complex, the class may contain other classes that come from other When the Ignite transport layer receives a message it needs to figure out how to deserialize the bytes and for that it needs to know the bundle that provides the class to be deserailized. To make things more complex, the class may contain other classes that come from other bundles, and so on recursively. In general, what is needed then is a way to map an FQN of a class to its bundle (and hence to the class loader).
...
It's responsibility of the implementation to ensure that the encoded representation is sufficient to unambiguously identify the correct bundle during deserialization.
The ClassLoaderCodec
should be called for every Object during serialization and deserialization and should be part of the IgniteConfiguraiton
:
Code Block |
---|
public interface ClassLoaderCodec {
@Nullable public Object encodeClassLoader(Class<?> cls, ClassLoader clsLdr) throws IgniteException;
public ClassLoader decodeClassLoader(String fqn, @Nullable Object encodedClsLdr) throws IgniteException;
} |
Ignite will come with 2 OSGI class loader codecs out of the box, pessimistic
and optimistic
, leaving users with opportunity to provide their own custom class loader codecs as well (potentially for non-OSGI environments).
In general in OSGi, the same package may be exported by multiple bundles and therefore an FQN may not be sufficient to look up the correct class loader. In such cases, the codec implementation must employ a pessimistic
approach and encode enough information (for example, the bundle symbolic name, plus the bundle version) for the deserializer to be able to resolve the FQN to the correct class loader. Such implementation will work for all use cases, but it introduces some overhead and increases the size of the serialized messages.
However, for the applications that can enforce one-to-one mapping of packages to bundles, a simplified (optimistic) approach can be used instead. With this approach, no encoding of the class loader is required (encodeClassLoader()
returns null
), and only the FQN is used for decoding of the class loader.
Here's how the pessimistic codec implementation might look like (in pseudo-code):
Code Block |
---|
public class ClassLoaderPessimisticCodec implements ClassLoaderCodec {
public ClassLoaderPessimisticCodec() {}
@Nullable public Object encodeClassLoader(Class<?> cls, ClassLoader clsLdr) throws IgniteException {
// TODO
return bundleName + bundleVersion;
}
public ClassLoader decodeClassLoader(String fqn, @Nullable Object encodedClsLdr) throws IgniteException {
// TODO: get class loader for a bundle based on bundleName and version.
...
}
} |
Here's how the optimistic
(opportunistic :)))) codec implementation might look like:
Code Block | ||
---|---|---|
| ||
public class ClassLoaderOptimisticCodec implements ClassLoaderCodec {
public ClassLoaderOptimisticCodec() {}
@Nullable public Object encodeClassLoader(Class<?> cls, ClassLoader clsLdr) throws IgniteException {
return null;
}
public ClassLoader decodeClassLoader(String fqn, @Nullable Object encodedClsLdr) throws IgniteException {
// TODO:
// Iterate through all the bundles and pick the first one
// that can load the class. Once found, cache the class loader
// for faster lookups going forward.
...
}
} |
First of all the both approaches imply that your cluster is consistent and contains the same version of the bundles on all the nodes. This can be see a a valid assumption in order to ensure the consistency of your computation tasks. If you want to be able to work it in a more non deterministic approach then we have to introduce yet another strategy. But first let focus assume that the bundles are equals on the entire cluster.
TBD
On the write side this approach require you to capture the bundle symbolic name and its version. This is something easy to do as in OSGi all classloader except the system classloader implements the BundleReference. The pessimis codec can look like that:
[raul.kripalani]: The naming is confusing. We are actually not transmitting classloaders. In fact, we cannot do so. What we'd like to do is transmit deserialisation "hints", that are used in whatever form the marshaller deems appropriate. So if anything, I would call this class a DeserialisationHintsCodec with methods: generateHints and computeClassLoaderFromHints.
[dmitriy setrakyan]: I am not sure I see the reason for removing the word classLoader on serialization part and keeping it on deserialization. I also think that the method names should be symmetric. With that in mind, "encodeClassLoader" and "decodeClassLoader" may not be the best names, but they are consistent with each other and symmetric. My vote would be to keep the naming.
The ClassLoaderCodec
should be called for every Object during serialization and deserialization and should be part of the IgniteConfiguraiton
:
Code Block |
---|
public interface ClassLoaderCodec {
@Nullable public Object encodeClassLoader(Class<?> cls, ClassLoader clsLdr) throws IgniteException;
public ClassLoader decodeClassLoader(String fqn, @Nullable Object encodedClsLdr) throws IgniteException;
} |
[raul.kripalani]: See my comment above.
Ignite will come with 2 OSGI class loader codecs out of the box, pessimistic
and optimistic
, leaving users with opportunity to provide their own custom class loader codecs as well (potentially for non-OSGI environments).
In general in OSGi, the same package may be exported by multiple bundles and therefore an FQN may not be sufficient to look up the correct class loader. In such cases, the codec implementation must employ a pessimistic
approach and encode enough information (for example, the bundle symbolic name, plus the bundle version) for the deserializer to be able to resolve the FQN to the correct class loader. Such implementation will work for all use cases, but it introduces some overhead and increases the size of the serialized messages.
However, for the applications that can enforce one-to-one mapping of packages to bundles, a simplified (optimistic) approach can be used instead. With this approach, no encoding of the class loader is required (encodeClassLoader()
returns null
), and only the FQN is used for decoding of the class loader.
[raul.kripalani]: I don't like transmitting bundle symbolic names over the wire, as it couples the serialising party with the deserialising party, forcing both to contain the class inside the same bundle. As I said in the mailing list, making this assumption would be a short-sighted strategy, as users may be sharing caches across applications across multiple containers, where classes live in different bundles in different containers.
I also don't think it's necessary. We just need the package name + package version. An OSGi container cannot expose the same package under the same version number twice, so the tuple (package name, package version) is enough to unambiguously locate the Bundle that exports our class.
Now, what we need to do is determine HOW we locate the Bundle. I have two ideas in mind:
With either of these approaches, I think we don't need pessimistic and/or optimistic strategies. Just a single strategy would be enough.
Here's how the pessimistic codec implementation might look like (in pseudo-code):
Code Block |
---|
Code Block | language | java
public class ClassLoaderPessimisticCodec implements ClassLoaderCodec { public private static final byte FRAMEWORK_CLASS_LOADER_ID = 0; private static final byte IGNITE_CLASS_LOADER_ID = 1;ClassLoaderPessimisticCodec() {} @Nullable public Object encodeClassLoader(Class<?> cls, ClassLoader clsLdr) throws IgniteException { private static final byte BOOT_CLASS_LOADER_ID = 2; // TODO private static final byte BUNDLE_CLASS_LOADER_ID = 4; return bundleName + bundleVersion; private static final ClassLoader FRAMEWOR_CLASS_LOADER = Bundle.class.getClassLoader(); } public ClassLoader decodeClassLoader(String fqn, @Nullable Object encodedClsLdr) throws IgniteException { private final PackageAdmin packageAdmin; // TODO: get publicclass ClassLoaderPessimisticCodec(PackageAdmin packageAdmin) { this.packageAdmin = packageAdmin;loader for a bundle based on bundleName and version. } @Nullable... @Override public Object encodeClassLoader(Class<?> cls) throws IgniteException {} } |
Here's how the optimistic
(opportunistic :)))) codec implementation might look like:
Code Block | ||
---|---|---|
| ||
public class ClassLoaderOptimisticCodec implements ClassLoaderCodec { ClassLoader classLoader = cls.getClassLoader(); public ClassLoaderOptimisticCodec() {} @Nullable public Object if (isIgniteClass(classLoader)) { encodeClassLoader(Class<?> cls, ClassLoader clsLdr) throws IgniteException { return ClassLoaderDesc.newIgniteClassLoaderDesc()null; } public ClassLoader decodeClassLoader(String fqn, @Nullable Object encodedClsLdr) throws if (isFrameworkClassLoader(cls.getClassLoader())) IgniteException { return ClassLoaderDesc.newFrameworkClassLoader();// TODO: } // Iterate through all the bundles and pick Bundlethe bundle = FrameworkUtil.getBundle(cls); first one if// (bundlethat !=can null)load { the class. Once found, cache the class loader return ClassLoaderDesc.newBundleClassLoaderDesc(bundle); // for faster lookups going } forward. return ClassLoaderDesc.newBootClassLoader();... } } |
First of all the both approaches imply that your cluster is consistent and contains the same version of the bundles on all the nodes. This can be see a a valid assumption in order to ensure the consistency of your computation tasks. If you want to be able to work it in a more non deterministic approach then we have to introduce yet another strategy. But first let focus assume that the bundles are equals on the entire cluster.
TBD >= 5.0
On the write side this approach require you to capture the bundle symbolic name and its version. This is something easy to do as in OSGi all classloader except the system classloader implements the BundleReference. The pessimis codec can look like that:
Code Block | ||
---|---|---|
| ||
public class ClassLoaderPessimisticCodec implements ClassLoaderCodec { private static final byte FRAMEWORK_CLASS_LOADER_ID = 0; private static final byte IGNITE_CLASS_LOADER_ID = 1; private static final byte BOOT_CLASS_LOADER_ID = 2; private static final byte BUNDLE_CLASS_LOADER_ID = 4; private static final ClassLoader FRAMEWOR_CLASS_LOADER = Bundle.class.getClassLoader(); private final PackageAdmin packageAdmin; public ClassLoaderPessimisticCodec(PackageAdmin packageAdmin) { @Nullable private Bundle getBundleExt(Class<?> cls) { // maybe handle SecurityManager Bundle bundle = FrameworkUtil.getBundle(cls); if (bundle == null && isFrameworkClassLoader(cls.getClassLoader())) { bundle = FrameworkUtil.getBundle(Bundle.class); } return bundle; } @Override public ClassLoader decodeClassLoader(String fqn, ClassLoader clsLdr, @Nullable Object encodedClsLdr) throws IgniteException { ClassLoaderDesc classLoaderDesc = (ClassLoaderDesc) encodedClsLdr; switch (classLoaderDesc.classLoaderId) { case BOOT_CLASS_LOADER_ID: return clsLdr; this.packageAdmin case FRAMEWORK_CLASS_LOADER_ID:= packageAdmin; } @Nullable return FRAMEWOR_CLASS_LOADER;@Override public Object encodeClassLoader(Class<?> cls) throws case IGNITE_CLASS_LOADER_ID:IgniteException { ClassLoader classLoader return ClassLoaderCodec.class= cls.getClassLoader(); if case BUNDLE_CLASS_LOADER_ID:(isIgniteClass(classLoader)) { return ClassLoaderDesc.newIgniteClassLoaderDesc(); //strict version but we can think} about an different strategy here like minor or micro version rangeif (isFrameworkClassLoader(cls.getClassLoader())) { return packageAdmin.getBundles(classLoaderDesc.bsn, classLoaderDesc.versionClassLoaderDesc.newFrameworkClassLoader(); } return nullBundle bundle = FrameworkUtil.getBundle(cls); } staticif final(bundle class!= ClassLoaderDescnull) implements{ Externalizable { private String versionreturn ClassLoaderDesc.newBundleClassLoaderDesc(bundle); private} String bsn; return private byte classLoaderId;ClassLoaderDesc.newBootClassLoader(); } @Override public ClassLoaderDesc(byte classLoaderId) {ClassLoader decodeClassLoader(String fqn, ClassLoader clsLdr, @Nullable Object encodedClsLdr) this.classLoaderIdthrows =IgniteException classLoaderId;{ ClassLoaderDesc classLoaderDesc } = (ClassLoaderDesc) encodedClsLdr; publicswitch ClassLoaderDesc(Bundle bundle(classLoaderDesc.classLoaderId) { this.classLoaderId = BUNDLEcase BOOT_CLASS_LOADER_ID;: this.bsn = bundle.getSymbolicName() return clsLdr; this.version = bundle.getVersion().toString(); case FRAMEWORK_CLASS_LOADER_ID: } @Overridereturn FRAMEWOR_CLASS_LOADER; public void writeExternal(ObjectOutput out) throws IOException {case IGNITE_CLASS_LOADER_ID: out.write(classLoaderId); return ClassLoaderCodec.class.getClassLoader(); if (classLoaderId ==case BUNDLE_CLASS_LOADER_ID) {: out.writeUTF(bsn); //can be optimized //strict version but we can think about an different strategy here like minor or micro version range Bundle[] bundles out= packageAdmin.writeUTF(getBundles(classLoaderDesc.bsn, classLoaderDesc.version); } if (bundles == null) } { @Override public void readExternal(ObjectInput in) throwsthrow IOException, ClassNotFoundException { classLoaderId = in.readByte(new IgniteException("No bundle found: " + classLoaderDesc.bsn + ":" + classLoaderDesc.version); if (classLoaderId == BUNDLE_CLASS_LOADER_ID) { } } try { } static ClassLoaderDesc newIgniteClassLoaderDesc() { //highest ranking bundle return new ClassLoaderDesc(IGNITE_CLASS_LOADER_ID); } return public static ClassLoaderDesc newBundleClassLoaderDesc(Bundle bundle) { bundles[0].loadClass(fqn).getClassLoader(); } catch return(ClassNotFoundException new ClassLoaderDesc(bundle); e) { } public staticthrow ClassLoaderDescnew newFrameworkClassLoaderIgniteException(e) {; return new ClassLoaderDesc(FRAMEWORK_CLASS_LOADER_ID); } } default: public staticthrow ClassLoaderDescnew newBootClassLoaderIgniteException()"Unsupported { class loader description type: " + classLoaderDesc.classLoaderId); return new ClassLoaderDesc(BOOT_CLASS_LOADER_ID); } } } |
}
}
static final class ClassLoaderDesc implements Externalizable {
private String version;
private String bsn;
private byte classLoaderId;
public ClassLoaderDesc() {}
public ClassLoaderDesc(byte classLoaderId) {
this.classLoaderId = classLoaderId;
}
public ClassLoaderDesc(Bundle bundle) {
this.classLoaderId = BUNDLE_CLASS_LOADER_ID;
this.bsn = bundle.getSymbolicName();
this.version = bundle.getVersion().toString();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.write(classLoaderId);
if (classLoaderId == BUNDLE_CLASS_LOADER_ID) {
out.writeUTF(bsn);
//can be optimized
out.writeUTF(version);
}
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
classLoaderId = in.readByte();
if (classLoaderId == BUNDLE_CLASS_LOADER_ID) {
}
}
static ClassLoaderDesc newIgniteClassLoaderDesc() {
return new ClassLoaderDesc(IGNITE_CLASS_LOADER_ID);
}
public static ClassLoaderDesc newBundleClassLoaderDesc(Bundle bundle) {
return new ClassLoaderDesc(bundle);
}
public static ClassLoaderDesc newFrameworkClassLoader() {
return new ClassLoaderDesc(FRAMEWORK_CLASS_LOADER_ID);
}
public static ClassLoaderDesc newBootClassLoader() {
return new ClassLoaderDesc(BOOT_CLASS_LOADER_ID);
}
}
} |
Disclaimer: this implementation is not functional and not optimized is purpose it to show how it can be done. Here we are using the PackageAdmin
service which is deprecated but really simple to demonstrate the purpose.
Warning |
---|
It seems to be more interesting to get the |
In this strategy we start with a more strict assumption: packages of all serialized classes come from one and only one bundle.
You may think this option is more simple but it is not true.
TODO