IGNITE-17366 Ignite services middleware documentation and usage example (#10162)
diff --git a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/EchoService.java b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/EchoService.java
new file mode 100644
index 0000000..70a7227
--- /dev/null
+++ b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/EchoService.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.examples;
+
+import org.apache.ignite.services.Service;
+
+public interface EchoService extends Service {
+ /**
+ * Shows how to read parameter implicitly passed to the service.
+ */
+ String hello();
+}
diff --git a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/EchoServiceAccessInterceptor.java b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/EchoServiceAccessInterceptor.java
new file mode 100644
index 0000000..9137c52
--- /dev/null
+++ b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/EchoServiceAccessInterceptor.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.examples;
+
+import java.util.concurrent.Callable;
+import org.apache.ignite.services.ServiceCallContext;
+import org.apache.ignite.services.ServiceCallInterceptor;
+import org.apache.ignite.services.ServiceContext;
+
+//tag::service-call-interceptor[]
+public class EchoServiceAccessInterceptor implements ServiceCallInterceptor {
+ /** {@inheritDoc} */
+ @Override public Object invoke(String mtd, Object[] args, ServiceContext ctx, Callable<Object> next) throws Exception {
+ ServiceCallContext callCtx = ctx.currentCallContext();
+
+ if (callCtx == null || callCtx.attribute("user") == null)
+ throw new SecurityException("Anonymous access is restricted.");
+
+ return next.call();
+ }
+}
+//end::service-call-interceptor[]
diff --git a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/EchoServiceImpl.java b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/EchoServiceImpl.java
new file mode 100644
index 0000000..cfa9348
--- /dev/null
+++ b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/EchoServiceImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.examples;
+
+import org.apache.ignite.resources.ServiceContextResource;
+import org.apache.ignite.services.ServiceCallContext;
+import org.apache.ignite.services.ServiceContext;
+
+public class EchoServiceImpl implements EchoService {
+ /** Serial version UID. */
+ private static final long serialVersionUID = 0L;
+
+ //tag::service-call-context-read[]
+ @ServiceContextResource
+ private ServiceContext ctx;
+
+ /** {@inheritDoc} */
+ @Override public String hello() {
+ ServiceCallContext callCtx = ctx.currentCallContext();
+
+ String proxyUser = callCtx != null ? callCtx.attribute("user") : null;
+
+ return String.format("Hello %s!", (proxyUser == null || proxyUser.isEmpty() ? "anonymous" : proxyUser));
+ }
+ //end::service-call-context-read[]
+}
diff --git a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java
index 997613c..cd9218b 100644
--- a/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java
+++ b/docs/_docs/code-snippets/java/src/main/java/org/apache/ignite/snippets/services/ServiceExample.java
@@ -23,7 +23,7 @@
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.lang.IgnitePredicate;
import org.apache.ignite.services.ServiceConfiguration;
-import org.junit.jupiter.api.Test;
+import org.junit.Test;
public class ServiceExample {
@@ -173,7 +173,7 @@
ignite.services().deploy(serviceCfg);
// NOTE: work via proxy. Direct references like 'IgniteServices#service()' corrupt the statistics.
- MyCounterService svc = ignite.services().serviceProxy("myService", MyCounterService.class, true)
+ MyCounterService svc = ignite.services().serviceProxy("myService", MyCounterService.class, true);
//end::start-with-statistics[]
ignite.close();
@@ -197,4 +197,27 @@
//end::service-configuration[]
ignite.close();
}
+
+ @Test
+ public void serviceCallContextExample() {
+ //tag::service-call-context-create[]
+ try (Ignite ignite = Ignition.start()) {
+ ignite.services().deployClusterSingleton("EchoService", new EchoServiceImpl());
+
+ // Create context.
+ ServiceCallContext johnCtx = ServiceCallContext.builder().put("user", "John").build();
+ ServiceCallContext maryCtx = ServiceCallContext.builder().put("user", "Mary").build();
+
+ // Bind it to the service proxy.
+ EchoService johnProxy = ignite.services().serviceProxy("EchoService", EchoService.class, false, johnCtx);
+ EchoService maryProxy = ignite.services().serviceProxy("EchoService", EchoService.class, false, maryCtx);
+
+ // Prints "Hello John!".
+ System.out.println(johnProxy.hello());
+
+ // Prints "Hello Mary!".
+ System.out.println(maryProxy.hello());
+ }
+ //end::service-call-context-create[]
+ }
}
diff --git a/docs/_docs/images/service-intercept--order.png b/docs/_docs/images/service-intercept--order.png
new file mode 100644
index 0000000..001a0c6
--- /dev/null
+++ b/docs/_docs/images/service-intercept--order.png
Binary files differ
diff --git a/docs/_docs/services/services.adoc b/docs/_docs/services/services.adoc
index 6943097..ce72603 100644
--- a/docs/_docs/services/services.adoc
+++ b/docs/_docs/services/services.adoc
@@ -15,6 +15,9 @@
= Services
:javaFile: {javaCodeDir}/services/ServiceExample.java
+:javaEchoServiceFile: {javaCodeDir}/services/EchoServiceImpl.java
+:javaServiceInterceptorFile: {javaCodeDir}/services/EchoServiceAccessInterceptor.java
+
== Overview
A service is a piece of functionality that can be deployed to an Ignite cluster and execute specific operations.
@@ -291,6 +294,78 @@
an issue for real jobs like working with a DB or a cache.
====
+== Service Middleware
+
+WARNING: This feature is experimental and may change in future releases.
+
+[CAUTION]
+====
+This feature may affect performance of service execution.
+====
+
+Apache Ignite allows users to implement custom middleware logic for their services and provides the following features:
+
+1. Ability to implicitly pass custom immutable parameters from proxy to service (similar to request headers).
+2. Ability to define custom interceptor for service method calls.
+
+=== Service Call Context
+This feature allows users to implicilty pass parameters to any service method without service re-deployment.
+
+The user can create context and bind it to the service proxy. After that, each call to the proxy method will also implicitly pass context parameters to the service.
+
+[tabs]
+--
+tab:Java[]
+[source, java]
+----
+include::{javaFile}[tags=service-call-context-create, indent=0]
+----
+tab:C#/.NET[]
+--
+
+The user can read the `call context` using the `currentCallContext` method of `ServiceContext`.
+
+NOTE: Service call context is only accessible from the current thread during the execution of a service method.
+
+[tabs]
+--
+tab:Java[]
+[source, java]
+----
+include::{javaEchoServiceFile}[tags=service-call-context-read, indent=0]
+----
+tab:C#/.NET[]
+--
+
+=== Service Interceptor
+This feature allows users to intercept the call to any service method except
+lifecycle methods (`init()`, `execute()` and `cancel()`).
+
+The user can specify one or more interceptors in the `ServiceConfiguration`. They are deployed with the service using the same class loader.
+
+Each interceptor invokes the next interceptor in the chain using a delegated call, the last interceptor calls the service method.
+So, the interceptor specified first in the configuration is the last interceptor processing the result of the service method execution.
+
+image::images/service-intercept--order.png[invocation order]
+
+[CAUTION]
+====
+An incorrect interceptor implementation can lead to undefined behaviour of the service execution.
+====
+
+[tabs]
+--
+tab:Java[]
+[source, java]
+----
+include::{javaServiceInterceptorFile}[tags=service-call-interceptor, indent=0]
+----
+tab:C#/.NET[]
+--
+
+=== Complete Example
+link:{githubUrl}/examples/src/main/java/org/apache/ignite/examples/servicegrid/ServiceMiddlewareExample.java[ServiceMiddlewareExample.java, window=_blank]
+
// TODO: add how to call java services from .NET
diff --git a/examples/src/main/java/org/apache/ignite/examples/servicegrid/ServiceMiddlewareExample.java b/examples/src/main/java/org/apache/ignite/examples/servicegrid/ServiceMiddlewareExample.java
new file mode 100644
index 0000000..8139c64
--- /dev/null
+++ b/examples/src/main/java/org/apache/ignite/examples/servicegrid/ServiceMiddlewareExample.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples.servicegrid;
+
+import java.util.Date;
+import java.util.concurrent.Callable;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.examples.ExampleNodeStartup;
+import org.apache.ignite.examples.ExamplesUtils;
+import org.apache.ignite.resources.LoggerResource;
+import org.apache.ignite.services.ServiceCallContext;
+import org.apache.ignite.services.ServiceCallInterceptor;
+import org.apache.ignite.services.ServiceConfiguration;
+import org.apache.ignite.services.ServiceContext;
+
+/**
+ * The example shows how to add a middleware layer for distributed services in Ignite.
+ * <p>
+ * To start remote nodes, run {@link ExampleNodeStartup} in another JVM. It will start
+ * a node with {@code examples/config/example-ignite.xml} configuration.
+ * <p>
+ * NOTE:<br/>
+ * Starting {@code ignite.sh} directly will not work, as distributed services (and interceptors)
+ * cannot be peer-deployed and classes must be in the classpath for each node.
+ */
+public class ServiceMiddlewareExample {
+ /** Service name. */
+ private static final String SVC_NAME = "MapService";
+
+ /**
+ * Executes example.
+ *
+ * @param args Command line arguments, none required.
+ * @throws Exception If example execution failed.
+ */
+ public static void main(String[] args) throws Exception {
+ // Mark this node as client node.
+ Ignition.setClientMode(true);
+
+ try (Ignite ignite = Ignition.start("examples/config/example-ignite.xml")) {
+ if (!ExamplesUtils.hasServerNodes(ignite))
+ return;
+
+ ServiceCallInterceptor audit = new Audit();
+ ServiceCallInterceptor access = (mtd, args0, ctx, next) -> {
+ ServiceCallContext callCtx = ctx.currentCallContext();
+
+ if (callCtx == null || callCtx.attribute("user") == null || callCtx.attribute("user").isEmpty())
+ throw new SecurityException("Anonymous access is restricted.");
+
+ return next.call();
+ };
+
+ ServiceConfiguration cfg = new ServiceConfiguration()
+ .setName(SVC_NAME)
+ .setService(new SimpleMapServiceImpl<>())
+ .setTotalCount(1)
+ .setInterceptors(audit, access);
+
+ try {
+ ignite.services().deploy(cfg);
+
+ // Create proxy without context.
+ SimpleMapService<Object, Object> proxy =
+ ignite.services().serviceProxy(SVC_NAME, SimpleMapService.class, false);
+
+ try {
+ System.out.println("Try to call the proxy method without context.");
+ // The method call will be intercepted with a SecurityException because no username was provided.
+ proxy.put(0, 0);
+ }
+ catch (IgniteException expected) {
+ expected.printStackTrace();
+ }
+
+ // Create a service call context with the username.
+ ServiceCallContext callCtx = ServiceCallContext.builder().put("user", "John").build();
+
+ // Bind it to the service proxy.
+ proxy = ignite.services().serviceProxy(SVC_NAME, SimpleMapService.class, false, callCtx);
+
+ System.out.println("Call the proxy method with context.");
+
+ for (int i = 0; i < 10; i++)
+ proxy.put(i, i);
+
+ for (int i = 0; i < 5; i++)
+ proxy.get(i * 2);
+
+ System.out.println("Map size: " + proxy.size());
+ }
+ finally {
+ ignite.services().cancelAll();
+ }
+ }
+ }
+
+ /** */
+ private static class Audit implements ServiceCallInterceptor {
+ /** Serial version UID. */
+ private static final long serialVersionUID = 0L;
+
+ /** Injected Ignite logger. */
+ @LoggerResource
+ private IgniteLogger log;
+
+ /** {@inheritDoc} */
+ @Override public Object invoke(String mtd, Object[] args, ServiceContext ctx, Callable<Object> next) throws Exception {
+ String serviceName = ctx.name();
+ ServiceCallContext callCtx = ctx.currentCallContext();
+ String user = callCtx == null ? null : callCtx.attribute("user");
+
+ recordEvent(user, serviceName, mtd, "start");
+
+ try {
+ // Execute service method.
+ Object res = next.call();
+
+ // Record finish event after execution of the service method.
+ recordEvent(user, serviceName, mtd, "result " + res);
+
+ return res;
+ }
+ catch (Exception e) {
+ log.error("Intercepted error", e);
+
+ // Record error.
+ recordEvent(user, serviceName, mtd, "error: " + e.getMessage());
+
+ // Re-throw exception to initiator.
+ throw e;
+ }
+ }
+
+ /**
+ * Record an event to current server node console output.
+ */
+ private void recordEvent(String user, String svc, String mtd, String msg) {
+ System.out.printf("[%tT][%s][%s][%s] %s%n", new Date(), user, svc, mtd, msg);
+ }
+ }
+}
diff --git a/examples/src/test/java/org/apache/ignite/examples/IgniteServiceExamplesSelfTest.java b/examples/src/test/java/org/apache/ignite/examples/IgniteServiceExamplesSelfTest.java
new file mode 100644
index 0000000..64634b2
--- /dev/null
+++ b/examples/src/test/java/org/apache/ignite/examples/IgniteServiceExamplesSelfTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples;
+
+import org.apache.ignite.examples.servicegrid.ServiceMiddlewareExample;
+import org.apache.ignite.examples.servicegrid.ServicesExample;
+import org.apache.ignite.testframework.junits.common.GridAbstractExamplesTest;
+import org.junit.Test;
+
+/**
+ * Ignite service examples self test.
+ */
+public class IgniteServiceExamplesSelfTest extends GridAbstractExamplesTest {
+ /** {@inheritDoc} */
+ @Override protected void beforeTest() throws Exception {
+ startRemoteNodes();
+ }
+
+ /**
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testServicesExample() throws Exception {
+ ServicesExample.main(EMPTY_ARGS);
+ }
+
+ /**
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testServicesMiddlewareExample() throws Exception {
+ ServiceMiddlewareExample.main(EMPTY_ARGS);
+ }
+}
diff --git a/examples/src/test/java/org/apache/ignite/testsuites/IgniteExamplesSelfTestSuite.java b/examples/src/test/java/org/apache/ignite/testsuites/IgniteExamplesSelfTestSuite.java
index b26f311..3d0db2c 100644
--- a/examples/src/test/java/org/apache/ignite/testsuites/IgniteExamplesSelfTestSuite.java
+++ b/examples/src/test/java/org/apache/ignite/testsuites/IgniteExamplesSelfTestSuite.java
@@ -35,6 +35,7 @@
import org.apache.ignite.examples.EncryptedCacheExampleSelfTest;
import org.apache.ignite.examples.EventsExamplesMultiNodeSelfTest;
import org.apache.ignite.examples.EventsExamplesSelfTest;
+import org.apache.ignite.examples.IgniteServiceExamplesSelfTest;
import org.apache.ignite.examples.LifecycleExamplesSelfTest;
import org.apache.ignite.examples.MLTutorialExamplesSelfTest;
import org.apache.ignite.examples.MemcacheRestExamplesMultiNodeSelfTest;
@@ -84,6 +85,7 @@
TaskExamplesMultiNodeSelfTest.class,
MemcacheRestExamplesMultiNodeSelfTest.class,
MonteCarloExamplesMultiNodeSelfTest.class,
+ IgniteServiceExamplesSelfTest.class,
// Binary.
CacheClientBinaryExampleTest.class,