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,