Ship your function
Now a days function as service(FaaS) is trending in serverless area and it is enabling new opportunity that allows to send function on the fly to server and it will start executing immediately.
This is helps in building application that adapts to changing users needs very quickly.
Function_as_a_service is popular offering from cloud provider like Amazon , Microsoft, Google etc.
FaaS has lot of similarity with Actor model that talks about sending message to Actors and they perform local action, if code can be also treated like data then code can also be sent to remote process and it can execute function locally.
I remember Joe Armstrong talking about how during time when he was building Erlang he used to send function to server to become HTTP server or smtp server etc. He was doing this in 1986!
Lets look at how we can save executable function and execute it later.
I will use java as a example but it can be done in any language that allows dynamic linking. Javascript will be definitely winner in dynamic linking.
Quick revision
Lets have quick look at functions/behavior in java
@Test public void square_number() { Function<Integer, Integer> sqr = x -> x * x; assertEquals(4, sqr.apply(2)); assertEquals(9, sqr.apply(3)); assertEquals(16, sqr.apply(4)); } @Test public void to_upper() { Function<String, String> upper = x -> x.toUpperCase(); assertEquals("FAAS", upper.apply("FaaS")); }
Nothing much to explain above code, it is very basic transformation.
Save function
Lets try to save one of these function and see what happens.
@Test public void save_function() throws Exception { Function<String, String> upper = x -> x.toUpperCase(); try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream os = new ObjectOutputStream(bos)) { os.writeObject(upper); } }
Above code looks perfect but it fails at runtime with below error
java.io.NotSerializableException: faas.FunctionTest$$Lambda$266/1859039536 at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) at faas.FunctionTest.save_function(FunctionTest.java:39) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Lambda functions are not serializable by default.
Java has nice trick about using cast expression to add additional bound, more details are available at Cast Expressions.
In nutshell it will look something like below
@Test() public void save_function_works() throws Exception { // Addtional casting allow to mark as serilized Function<String, String> upper = (Function<String, String> & Serializable) x -> x.toUpperCase(); try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream os = new ObjectOutputStream(bos)) { os.writeObject(upper); try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream in = new ObjectInputStream(bis)) { Function<String, String> restoredUpper = (Function<String, String>) in.readObject(); Assertions.assertEquals("FAAS", restoredUpper.apply("FaaS")); } } }
This technique allows to convert any functional interface to bytes and reuse it later. It is used in JDK at various places like TreeMap/TreeSet as these data structure has comparator as function and also supports serialization.
With basic thing working lets try to build something more useful.
We have to hide & Serialized magic to make code more readable and this can be achieved by functional interface that extends from base interface and just adds Serializable, it will look something like below
@FunctionalInterface public interface SerFunction<T, R> extends Function<T, R>, Serializable { } @FunctionalInterface public interface SerPredicate<T> extends Predicate<T>, Serializable { } ....
Once we take care of boilerplate then it becomes very easy to write the functions that are Serialization ready.
List functions = asList( SerCode.f((Integer x) -> x * x), SerCode.f((String x) -> x.toUpperCase()), SerCode.p((String x) -> x.length() > 5) ); byte[] code = saveFunction(functions); ObjectInputStream fStream = codeStream(code); List rFunctions = (List) fStream.readObject(); int fIndex = 0; Function<Integer, Integer> rSquare = (Function<Integer, Integer>) rFunctions.get(fIndex++); System.out.println(rSquare.apply(10)); // Shows 100 Function<String, String> rUpper = (Function<String, String>) rFunctions.get(fIndex++); System.out.println(rUpper.apply("FaaS")); // Shows "FAAS Predicate<String> rGt5Length = (Predicate<String>) rFunctions.get(fIndex++); System.out.println(rGt5Length.test("I am greater than 5")); // Shows true
With above building block we can save full transformation(map/filter/reduce/collect etc) and ship to sever for processing. This also allows to build computation that can recomputed if required.
Spark is distributed processing engine that use such type of pattern where it persists transformation function and use that for doing computation on multiple nodes.
So next time you want to build some distributed processing framework then look into this pattern or want to take it to extreme then send patched function to live server in production to fix the issue.
Code used in in post is available @ faas
Published on Java Code Geeks with permission by Ashkrit Sharma, partner at our JCG program. See the original article here: Ship your function Opinions expressed by Java Code Geeks contributors are their own. |