Wednesday, July 22, 2020

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.

Oracle Java Tutorial and Material, Oracle Java Exam Prep, Oracle Java Study Matterials

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.

Oracle Java Tutorial and Material, Oracle Java Exam Prep, Oracle Java Study Matterials

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.

Related Posts

0 comments:

Post a Comment