Archived

This forum has been archived. Please start a new discussion on GitHub.

Using Ice Marshalling to build hashing and caching of expensive remote operations

Hello

Imagine a costly remote call, which may take a lot of time and may be called multiple times with the same argument, from multiple clients. The argument is a relatively rich data structure which is represented in terms of a class defined in Slice.

We would like to implement a caching mechanism on the server side: Once we already have calculated a result for a certain parameter, we do not recalculate it but take it right from the cache.

The first idea is to use the Ice Streaming interface to generate a byte array from the object, in the hope that structurally identical objects (i.e. different instances but with the same data) produce the same byte array. This works fine as long as we only use structs, enums and pod types, even dicts and sequences of those work fine. As soon as a data element is represented as a Slice class, objects with exactly the same data produce different byte arrays via the streaming interface. Is that intended, or is there a reason for that? Is there a way around this?

As we use ZeroC in .NET the second idea was to attach the Serializable attribute to each Slice declaration, for example like this

["cs:attribute:System.Serializable"]
class A
{
int mA;
};

["cs:attribute:System.Serializable"]
struct SerializableData
{
int i;
double d;
string s;
EnumType dataEnum;
EnumTypeDict dict;
A mA;
};

Again, this works fine for structs, but as soon as Slice classes come into play, we get a .NET serialization exception because the class inherit from ObjectImpl, which is not marked with the Serializable attribute.

System.Runtime.Serialization.SerializationException : Type 'Ice.ObjectImpl' in Assembly
'Ice, Version=3.4.2.0, Culture=neutral, PublicKeyToken=cdd571ade22f2f16' is not marked as
serializable.

If ObjectImpl would be serializable we may use .NET seralization, but eventually run into the same problem as with our first approach.

There is always the possibility to code a good hash key by hand and then compare objects with the same hash key to check for equality. However, this would require extension methods for the different data types defined in Slice and would add additional maintenance efforts and sources of errors.

Do you see another approach, which is going to solve our caching problem.

Many thanks for the help.

Regards, Daniel

Comments

  • bernard
    bernard Jupiter, FL
    Hi Daniel,

    The Ice encoding is documented in the manual and is completely deterministic. The encoding for class objects is more complicated than the encoding of other types; in particular, with the 1.0 encoding, you must read the entire parameter encapsulation, since the classes are encoded at the end of this encapsulation. See Data Encoding - Ice 3.5 - ZeroC.

    Perhaps you are attempting to compare only a subset of the parameters sent with your operation? Can you provide the Slice definitions where this comparison does not work?

    I would also recommend to upgrade to Ice 3.5 and try the new "compact" format for classes, which is simpler and could work better for you.

    Best regards,
    Bernard
  • Hi Bernard

    I distilled a small example which shows the effect. It is strange because in some other similar settings things work as expected.

    First the slice definition, it is just a sketch of some date structure.
    module TestSlice
    {
    	struct QuotableKey
    	{
    		string mKey;
    	};
    
    	struct DateTime
    	{
    		int mYear;
    		int mMonth;
    		int mDay;
    		int mHour;
    		int mMinute;
    		int mSecond;
    		int mMillisecond;
    	};
    				
    	class ExerciseMethod { };
    
    	class European extends ExerciseMethod { };
    	
    	enum OptionType { Call, Put };
    							
    	class Payoff { };
    
    	class PayoffOne extends Payoff { };
    
    	class PayoffIdentity extends Payoff { };
    
    	class PayoffVanilla extends Payoff
    	{
    		OptionType mOptionType;
    		double mStrike;
    	};
    
    	class ForwardStart { };
    
    	class StartImmediate extends ForwardStart { };
    
    	class FinancialProduct { }; 
    
    	class Vanilla extends FinancialProduct
    	{
    		double mNotional;
    		QuotableKey mQuotableKey;
    		DateTime mMaturityDate;
    		Payoff mPayoff;
    		ExerciseMethod mExercise;
    		ForwardStart mForwardStart;  // adding this field produced different byte array for structural identical objects
    	};
    };
    

    Then I use Ice .NET (via F#, but that does not really matter) to create identical objects and then the Stream interface to serialize them:
    [<Test>]
    let ``Test request compare through Ice byte stream``() =
    
        let notional = 100.0
        let strike = 20.0
        let maturity = TestSlice.DateTime(2012, 12, 1, 1, 0, 0, 0)
        let CHF = CurrencyCode("CHF")
    
        let product1 = TestSlice.Vanilla(notional, 
                                         TestSlice.QuotableKey("ABBN"), 
                                         maturity, 
                                         TestSlice.PayoffVanilla(TestSlice.OptionType.Call, strike),  
                                         TestSlice.European(),                               
                                         TestSlice.StartImmediate())  
    
        let product2 = TestSlice.Vanilla(notional, 
                                         TestSlice.QuotableKey("ABBN"), 
                                         maturity, 
                                         TestSlice.PayoffVanilla(TestSlice.OptionType.Call, strike),
                                         TestSlice.European(),
                                         TestSlice.StartImmediate())
    
        let initData = Ice.InitializationData()
        let communicator = Ice.Util.initialize(initData)
        let outputStream = Ice.Util.createOutputStream(communicator)
    
        try 
            // using write__ produces MarshalException, so go over helper
            TestSlice.FinancialProductHelper.write(outputStream, product1)
            outputStream.writePendingObjects()
            let bytes1 = outputStream.finished()
    
            outputStream.reset(true)
            TestSlice.FinancialProductHelper.write(outputStream, product2)
            outputStream.writePendingObjects()
            let bytes2 = outputStream.finished()
    
            Assert.AreEqual(bytes1.Length, bytes2.Length) // this passes
    
            let diff = 
                (Array.zip bytes1 bytes2) 
                |> Array.mapi (fun i (b1, b2) -> if b1 <> b2 then printfn "[%d]: %X <> %X" i b1 b2; 1 else 0) 
                |> Array.sum
    
            Assert.AreEqual(0, diff) // this failes with 176 differences, first at index 144
    
        finally
            outputStream.destroy()
    

    The test fails as indicated in the comments. Although the objects are structurally identical, they byte streams are different. Very strange is the following observation: as soon as I remove the member mForwardStart from the class so that
    	class Vanilla extends FinancialProduct
    	{
    		double mNotional;
    		QuotableKey mQuotableKey;
    		DateTime mMaturityDate;
    		Payoff mPayoff;
    		ExerciseMethod mExercise;
    		// if the next field is removed all works fine
    		//ForwardStart mForwardStart;  // adding this field produced different byte array for structural identical objects
    	};
    

    then the byte streams are identical. Can you imagine what is going wrong.
    I briefly browsed the documentation about the serialization format, and also looked at the generated code. From there I cannot immediately understand why such a behavior is implied.

    Many thanks for your feedback and help

    Daniel
  • xdm
    xdm La Coruña, Spain
    Hi Daniel,

    I have ported your test to C# and it seems to work fine with both 3.4.2 and 3.5.0

    I have used the same Slice definitions you posted, and here is my C# code.
    double notional = 100.0;
    double strike = 20.0;
    TestSlice.DateTime maturity = new TestSlice.DateTime(2012, 12, 1, 1, 0, 0, 0);
    
    TestSlice.Vanilla product1 = new TestSlice.Vanilla(notional, 
                                                       new TestSlice.QuotableKey("ABBN"), 
                                                       maturity, 
                                                       new TestSlice.PayoffVanilla(TestSlice.OptionType.Call, strike),  
                                                       new TestSlice.European(),                          
                                                       new TestSlice.StartImmediate());
    
    TestSlice.Vanilla product2 = new TestSlice.Vanilla(notional, 
                                                       new TestSlice.QuotableKey("ABBN"), 
                                                       maturity, 
                                                       new TestSlice.PayoffVanilla(TestSlice.OptionType.Call, strike),  
                                                       new TestSlice.European(),                          
                                                       new TestSlice.StartImmediate());
    
    Ice.InitializationData initData = new Ice.InitializationData();
    initData.properties = Ice.Util.createProperties();
    //initData.properties.setProperty("Ice.Default.EncodingVersion", "1.0");
    Ice.Communicator communicator = Ice.Util.initialize(initData);
    Ice.OutputStream outputStream = Ice.Util.createOutputStream(communicator);
    
    try
    {
        // using write__ produces MarshalException, so go over helper
        TestSlice.FinancialProductHelper.write(outputStream, product1);
        outputStream.writePendingObjects();
        byte[] bytes1 = outputStream.finished();
    
        outputStream.reset(true);
        TestSlice.FinancialProductHelper.write(outputStream, product2);
        outputStream.writePendingObjects();
        byte[] bytes2 = outputStream.finished();
    
        Debug.Assert(bytes1.Length == bytes2.Length); // this passes
        Debug.Assert(bytes1.SequenceEqual(bytes2));
        Console.WriteLine("Equals");
    }
    finally
    {
        outputStream.destroy();
    }
    

    do you see the same problems when using C# instead of F#?

    Best regards,
    Jose
  • Hi Jose

    I tested more and I think I could identify the issue. We are using Visual Studio 2012 with a custom built VS addin to support Ice 3.4.2 with Visual Studio 2012. The reason for using the older version is because it is supporting Python 2.6, which we need for integration with another third party software and we didn't wanted to make a special build of 3.5 to use Python 2.6.

    I realized that with this setup, the tests randomly fail, but sometimes also pass (less often though).

    I then reinstalled Ice 3.5 on a fresh machine and converted the Visual 2012 project to use Ice 3.5 directly. Then all tests passed, everything worked as expected.

    It seems to be a special interaction between VS 2012 and the custom build addin to support Ice 3.4.2.

    We investigate if we can move to Ice 3.5.

    Do you see any issues to compile it for Python 2.6?

    Many thanks for the help.

    Daniel
  • xdm
    xdm La Coruña, Spain
    Python 2.6 is still a supported platform so shouldn't be any problems. The Windows distribution just provide Python 3.3 binary packages, so you will need to build the 2.6 packages but should be straightforward.

    Let us know if you need further assistance with this.

    Best regards,
    Jose