Implementing Ignite.NET Plugin: Distributed Semaphore

Apache Ignite.NET 2.0 introduced plugin system. Plugins can be .NET-only or .NET + Java. Let’s see how to implement the latter.

Why would I need a plugin?

Ignite.NET is built on top of Ignite (which is written in Java). JVM is started within .NET process, and .NET part talks to Java part and reuses existing Ignite functionality where possible.

Plugin system exposes this platform interaction mechanism to the third parties. One of the main use cases is making Ignite and third party Java APIs available in .NET.

Good example of such an API is IgniteSemaphore, which is not yet available in Ignite.NET.

All source code for this post is available on GitHub: github.com/ptupitsyn/ignite-net-examples/tree/master/Plugin.

Distributed Semaphore API

Ignite Semaphore is very similar to System.Threading.Semaphore (MSDN), but the effect is cluster-wide: limit the number of threads executing a given piece of code across all Ignite nodes.

It should be used in C# code like this:

IIgnite ignite = Ignition.GetIgnite();
ISemaphore semaphore = ignite.GetOrCreateSemaphore(name: "foo", count: 3);

semaphore.WaitOne();  // Enter the semaphore (may block)
// Do work
semaphore.Release();

Looks simple enough and quite useful; same API as built-in .NET Semaphore. Obviously, we can’t change IIgnite interface, so GetOrCreateSemaphore is an extension method. Now onto the implementation!

Java Plugin

Let’s start with Java side of things. We need a way to call Ignite.semaphore() method there and provide access to the resulting instance to the .NET platform.

Create a Java project and reference Ignite 2.0 from Maven (detailed instructions can be found in Building Multi-Platform Ignite Cluster post).

Every plugin starts with PluginConfiguration. Our plugin does not need any configuration properties, but the class must exist, so just make a simple one:

public class IgniteNetSemaphorePluginConfiguration implements PluginConfiguration {}

Then comes the plugin entry point: PluginProvider<PluginConfiguration>. This interface has lots of methods, but most of them can be left empty (name and version must not be null, so put something in there). We are interested only in initExtensions method, which allows us to provide a cross-platform interoperation entry point. This is done by registering PlatformPluginExtension implementation:

public class IgniteNetSemaphorePluginProvider implements PluginProvider<IgniteNetSemaphorePluginConfiguration> {
    public String name() { return "DotNetSemaphore"; }
    public String version() { return "1.0"; }

    public void initExtensions(PluginContext pluginContext, ExtensionRegistry extensionRegistry) 
            throws IgniteCheckedException {
        extensionRegistry.registerExtension(PlatformPluginExtension.class,
                new IgniteNetSemaphorePluginExtension(pluginContext.grid()));
    }
...
}

PlatformPluginExtension has a unique id to retrieve it from .NET side and a PlatformTarget createTarget() method to create an object that can be invoked from .NET.

PlatformTarget interface in Java mirrors IPlatformTarget interface in .NET. When you call IPlatformTarget.InLongOutLong in .NET, PlatformTarget.processInLongOutLong is called in Java on your implementation. There are a number of other methods that allow exchanging primitives, serialized data, and objects. Each method has a type parameter which specifies an operation code, in case when there are many different methods on your plugin.

We are going to need two PlatformTarget classes: one that represents our plugin as a whole and has getOrCreateSemaphore method, and another one to represent each particular semaphore. First one should take a string name and int count and return an object, so we need to implement PlatformTarget.processInStreamOutObject. Other methods are not needed and can be left blank:

public class IgniteNetPluginTarget implements PlatformTarget {
    private final Ignite ignite;

    public IgniteNetPluginTarget(Ignite ignite) {
        this.ignite = ignite;
    }

    public PlatformTarget processInStreamOutObject(int i, BinaryRawReaderEx binaryRawReaderEx) throws IgniteCheckedException {
        String name = binaryRawReaderEx.readString();
        int count = binaryRawReaderEx.readInt();

        IgniteSemaphore semaphore = ignite.semaphore(name, count, true, true);

        return new IgniteNetSemaphore(semaphore);
    }
...
}

For each ISemaphore object in .NET there will be one IgniteNetSemaphore in Java, which is also a PlatformTarget. And this object will handle WaitOne and Release methods and delegate them to underlying IgniteSemaphore object. Since both of these methods are void and parameterless, the simplest PlatformTarget method will work:

public long processInLongOutLong(int i, long l) throws IgniteCheckedException {
    if (i == 0) semaphore.acquire();
    else semaphore.release();
    
    return 0;
}

That’s it, Java part is implemented! We just need to make our IgniteNetSemaphorePluginProvider class available to Java service loader by creating a resources\META-INF.services\org.apache.ignite.plugin.PluginProvider file with a single line containing the class name. Package the project with Maven (mvn package in console, or use IDEA UI). There should be a IgniteNetSemaphorePlugin-1.0-SNAPSHOT.jar file in the target directory. We can move on to the .NET part now.

.NET Plugin

First let’s make sure our Java code gets picked up by Ignite. Create a console project, install Ignite NuGet package, and start Ignite with the path to the jar file that we just created:

var cfg = new IgniteConfiguration
{
    JvmClasspath = @"..\..\..\..\Java\target\IgniteNetSemaphorePlugin-1.0-SNAPSHOT.jar"
};

Ignition.Start(cfg);

Ignite node starts up and we should see our plugin name in the log:

[16:02:38] Configured plugins:
[16:02:38]   ^-- DotNetSemaphore 1.0

Great! For the .NET part we’ll take an API-first approach: implement the extension method first and continue from there.

public static class IgniteExtensions
{
    public static Semaphore GetOrCreateSemaphore(this IIgnite ignite, string name, int count)
    {
        return ignite.GetPlugin<SemaphorePlugin>("semaphorePlugin").GetOrCreateSemaphore(name, count);
    }
}

For the GetPlugin method to work, IgniteConfiguration.PluginConfigurations property should be set. It takes a collection of IPluginConfiguration implementations, and each implementation must, in turn, link to a IPluginProvider implementation with an attribute:

[PluginProviderType(typeof(SemaphorePluginProvider))]
class SemaphorePluginConfiguration : IPluginConfiguration  {...}

On node startup Ignite.NET iterates through plugin configurations, instantiates plugin providers, and calls Start(IPluginContext<SemaphorePluginConfiguration> context) method on them. IIgnite.GetPlugin calls are then delegated to IPluginProvider.GetPlugin of the provider with specified name.

class SemaphorePluginProvider : IPluginProvider<SemaphorePluginConfiguration>
{
    private SemaphorePlugin _plugin;

    public T GetPlugin<T>() where T : class
    {
        return _plugin as T;
    }

    public void Start(IPluginContext<SemaphorePluginConfiguration> context)
    {
        _plugin = new SemaphorePlugin(context);
    }

    ...

}

IPluginContext provides access to Ignite instance, Ignite and plugin configurations, and has GetExtension method, which delegates to PlatformPluginExtension.createTarget() in Java. This way we “establish connection” between the two platforms. IPlatformTarget in .NET gets linked to PlatformTarget in Java; they can call each other, and the lifetime of Java object is tied to lifetime of .NET object. Once .NET object is reclaimed by the garbage collector, finalizer releases the Java object reference, and it will also be garbage collected.

Remaining implementation is simple - just call appropriate IPlatformTarget methods:

class SemaphorePlugin
{
    private readonly IPlatformTarget _target;  // Refers to IgniteNetPluginTarget in Java

    public SemaphorePlugin(IPluginContext<SemaphorePluginConfiguration> context)
    {
        _target = context.GetExtension(100);
    }

    public Semaphore GetOrCreateSemaphore(string name, int count)
    {
        var semaphoreTarget = _target.InStreamOutObject(0, w =>
        {
            w.WriteString(name);
            w.WriteInt(count);
        });

        return new Semaphore(semaphoreTarget);
    }
}

class Semaphore
{
    private readonly IPlatformTarget _target;  // Refers to IgniteNetSemaphore in Java

    public Semaphore(IPlatformTarget target)
    {
        _target = target;
    }

    public void WaitOne()
    {
        _target.InLongOutLong(0, 0);
    }

    public void Release()
    {
        _target.InLongOutLong(1, 0);
    }
}

We are done! Quite a bit of boilerplate code, but adding more logic to existing plugin is easy, just implement a pair of methods on both sides. Ignite uses JNI and unmanaged memory to exchange data between .NET and Java platforms within single process, which is simple and efficient.

Testing

To demonstrate distributed nature of our Semaphore, we can run multiple Ignite nodes where each of them calls WaitOne(). We’ll see that only two nodes at a time are able to acquire the semaphore:

var ignite = Ignition.Start(cfg);
var sem = ignite.GetOrCreateSemaphore("foo", 2);

Console.WriteLine("Trying to acquire semaphore...");

sem.WaitOne();

Console.WriteLine("Semaphore acquired. Press any key to release.");
Console.ReadKey();

Download full project from GitHub: github.com/ptupitsyn/ignite-net-examples/tree/master/Plugin.

Written on July 14, 2017