orbeon / orbeon-forms

Orbeon Forms is an open source web forms solution. It includes an XForms engine, the Form Builder web-based form editor, and the Form Runner runtime.
http://www.orbeon.com/
GNU Lesser General Public License v2.1
517 stars 221 forks source link

Session/state replication #1529

Closed ebruchez closed 6 years ago

ebruchez commented 10 years ago

Current status

As of 2011-09-26, the XForms engine session cannot be replicated because:

+1 from customer

avernet commented 8 years ago

+1 from community

ebruchez commented 7 years ago

We discussed today with @avernet that we could also use Ehcache replication.

ebruchez commented 7 years ago

Steps for a prototype:

Q: Any way to do anything using browser local state?

ebruchez commented 7 years ago

Tomat has a DeltaManager session manager which "only replicat[es] the deltas in data". However documentation doesn't stay what that actually means (how does the algorithm work?). It might be by session attribute.

ebruchez commented 7 years ago

What we store in the session:

What we store indirectly via UUID into caches:

ebruchez commented 7 years ago

Data: initial state for Controls form:

So we are talking about 100KB, mostly of instances.

Need more data of course.

ebruchez commented 7 years ago

One issue: unless there is a shared filesystem where temporary files, uploads which haven't been saved to a database would be lost when failing over.

ebruchez commented 7 years ago

This blog post from almost 6 years ago indicates about 120 MB/s with Ehcache via RMI for 60 KB objects, less as you increase object size (and about 50% of that with JGroups).

ebruchez commented 7 years ago
ebruchez commented 7 years ago
ebruchez commented 7 years ago

Unclear, based on this, whether using Ehcache for Tomcat session replication is possible independently from the rest of Shiro:

EHCache is also a nice choice if you quickly need container-independent session clustering. You can transparently plug in TerraCotta behind EHCache and have a container-independent clustered session cache. No more worrying about Tomcat, JBoss, Jetty, WebSphere or WebLogic specific session clustering ever again!

It seems that, maybe, Shiro creates its own sessions and you wouldn't use web sessions at all.

ebruchez commented 7 years ago

We have a few options:

  1. Do everything with the session. Then we have the problem of the static state, and of configurations which might be different between app servers.
  2. Do everything from Ehcache. In this case, we could move everything we store in the session to Ehcache, and index into Ehcache using the session id. Hopefully the session id is preserved across replicated servlet containers.
  3. Do a mix. This seems like it would be a pain as servlet session AND Ehcache would need to replicate similarly but with different configurations.

So I think that option 2 might be the best so far.

avernet commented 7 years ago

I agree. Also, with the "everything in the session" option, where would we store what we can't afford to keep in memory, and right now serialize to disk through Ehcache? It seems to me that anyway, we can't keep everything in the session.

ebruchez commented 7 years ago
ebruchez commented 7 years ago

Question raised by @avernet: if we do not store anything in the actual servlet session anymore, could we not use/check the session at all? Could this help with occasional issues users have with sessions in particular in embedded settings?

One idea is that the XForms document's UUID could be sufficient.

There are are few things where a setting by user can make sense, such as fr-language. It might be, at this point, the only setting shared per session.

ebruchez commented 7 years ago

We currently have a 2-level XForms document cache/store:

  1. In-memory cache of live XFormsContainingDocument
  2. When evicted from that cache, we put documents in the EhcacheStateStore, which creates an instance of DynamicState. This maps to the xforms.state cache, which is configured as a disk cache. Upon serializing to disk, Java serialization apples to DynamicState as it is a Scala case class which is a java.io.Serializable.

Given this, replication causes an issue, which is that after each Ajax request, the new state must be propagated.

Possibilities:

(BTW as discussed with @avernet state serialization is likely much cheaper than deserialization, as deserialization entails recreating and initializing an XFormsContainingDocument including RRR and creating the whole tree of controls, evaluating lots of XPath expressions, etc.)

ebruchez commented 7 years ago

To clarify, it's not entirely true we don't need to replicate sessions even if we store everything in Ehcache: session replication needs to be enabled for the servlet containers, otherwise the container won't be able to accept the incoming session cookie from the client, and we won't have a session id at our disposal. However, no session objects will have to be replicated in this case, except maybe fr-language or secondary user settings like this.

The above, of course, if we keep checking the session at all. If we don't, then from Orbeon Forms we wouldn't even need to replicate the session at all, and a purely servlet session-less operation would be possible.

In practice, I would imagine that session would be needed by most deployments, be it only to handle logged in users, unless some third-party solutions like Apache Shiro are used.

ebruchez commented 7 years ago

Defer:

ebruchez commented 7 years ago

We need 2 caches:

Both need to be replicated the same way.

ebruchez commented 7 years ago

One question is that, since we do need session replication after all, should we use Ehcache for a session cache? What is the benefit?

One thing is that, for the XForms dynamic state, we do passivation to disk. Now we could use the servlet session, and configure that to do passivation as well, although that would apply to the entire content of the user's session, and the configuration would be different from app server to app server.

avernet commented 7 years ago

Only being able to passivate the whole session, and instead of being able to do it a document-by-document-basis, seems like a step back. Imagine you have fairly active users, that load many forms, over hours or work. In such a scenario, you would keep in memory the dynamic state for all the "old forms" for those users, and would require much more RAM for the same amount of load.

Do containers, say Tomcat, have the ability to only passivate part of the session for a given user?

ebruchez commented 7 years ago

Do containers, say Tomcat, have the ability to only passivate part of the session for a given user?

I doubt it, Tomcat is pretty basic.

I agree that just session passivation would be a step back.

ebruchez commented 7 years ago

Summary so far:

avernet commented 7 years ago

Could you detail what you mean by "obtain a unique id to access other caches"?

Regarding "allow sticky sessions to work at the load balancer level", I don't think load balancer need the app server to maintain their own cookie. E.g. HAProxy can keep track of the server for a user by rewriting a given cookie, like JSESSIONID, doing 123s1~123 on responses, and s1~123123 on requests, but it can also use its own cookie, e.g. SERVERID=s1.

ebruchez commented 7 years ago

Found out why I get an "unexpected request sequence number" when moving back to server A. It's because server A still has a live document around. It doesn't know that server B processed some requests.

One solution might be, in case of sequence number error, to check if we can find one in the store, as that might have been updated in the background.

ebruchez commented 7 years ago

Terminology from HAProxy doc:

Using persistence, we mean that we’re 100% sure that a user will get redirected to a single server. Using affinity, we mean that the user may be redirected to the same server…

ebruchez commented 7 years ago

Solution for problem above:

ebruchez commented 7 years ago

So at this point we have shown that replication can work provided:

At this point, we don't yet know the performance implications for:

So we would need some numbers.

ebruchez commented 7 years ago

storeDocumentState takes about 5 ms (average of 100 serializations) for form with:

The size is 53,732 bytes, out of which 46,290 are for instances, which breaks down to:

fr-initial-instance is for keeping "initial data for clear button". It never changes after the initial load, so this would be a case where if we split the dynamic state into more than one cache key, we could prevent unneeded serializations and replication.

Editing that same form in Form Builder, storeDocumentState:

We should maybe consider disabling replication for Form Builder, for most deployment uses, as here replication might be more expensive. Seeing how we could reduce the size of serialized instances would be a good idea too. We should do space compression on instances (see #2751, #1715).

What about gzip compression of serialized data?

ebruchez commented 7 years ago

Dynamic state bytes compresses well by a factor of 7-8. For example:

Not sure why I have 20 ms above vs. 15 ms in my previous test the day before!

Q: Should we, and if so when, compress dynamic state when putting it into Ehcache? Is the tradeoff worth it?

ebruchez commented 7 years ago

Property:

<property 
    as="xs:boolean" 
    name="oxf.xforms.replication"         
    value="true"/>
ebruchez commented 7 years ago

One question is: when should the caches be initialized? We would like replication can happen this at the earliest. The OrbeonServletContextListener is a possibility. But this is not XForms-aware. So we should probably introduce a new ServletContextListener for this purpose and add it to web.xml. Suggesting org.orbeon.oxf.xforms.ReplicationServletContextListener.

ebruchez commented 7 years ago
ebruchez commented 7 years ago

XFormsServerSharedInstancesCache is used by Form Runner and Form Builder to cache resources, in particular form resources. Those loaded from oxf: are no problem, but those loaded with for example fr-get-fr-resources go through a local service.

What we should do is, if the URL is relative to the current host/port/context, store a path to be rewritten instead of the absolute URL. This way, when the path migrates to another server, it will be resolved again but against the current host.

ebruchez commented 7 years ago

Uploads are an issue. If an upload is in progress when a server goes down, then it will return an error, which is probably acceptable in most cases as the user will see the error and restart the upload. At least, I think this is ok in a first implementation.

If a server goes down after an upload has completed, then the form instance data points to a temporary file on disk. Unless the replica servers share the temp filesystem with the original server, the replica will now contain instance data with a temporary file URL which points to a missing file. When saving the data to the database, an error will probably happen.

So what should we do in this case?

  1. Is it realistic to replicate uploaded files? They can be arbitrarily large, and we use Ehcache to replicate so we would have to find a way to replicate large amounts of data this way. Will Ehcache scale? On the other hand, if we switched to Ehcache for temporary attachments instead of using temporary files, this might work. But it is clearly more implementation work.
  2. Another "solution" is to detect this situation, hoping that it will be rare and that most forms have at most a few attachments. At some point, maybe before saving, attachment paths should be checked. If a file is not present, then the user should be notified and saving should be interrupted.
ebruchez commented 7 years ago

I think that the sponsor for this feature doesn't need attachments at first. But we should probably at least do solution 2 above. We could make this part of require-uploads. This calls pending-uploads (tryPendingUploads). Either tryPendingUploads or another Form Runner action could check the status of unsaved attachment.

If unsaved attachments have missing files:

Another possibility could be to have a standard validation for attachments which checks the existence of the associated file. If the file disappears, the control would show in error. But it could be costly to check all attachments all the time without optimization. With replication, this would only be needed upon state restoration.

ebruchez commented 7 years ago

In #1067 we discussed an xxforms-state-restored event which, possibly, could be also used to check attachments. (#3317)

ebruchez commented 7 years ago

For uploads, I have implemented a simple solution to detect missing attachments and show an error.

ebruchez commented 7 years ago
 java.io.NotSerializableException: org.orbeon.oxf.xforms.state.XFormsStateManager
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1183)
    at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1547)
    at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1508)
    at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1431)
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1177)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:347)
    at org.apache.catalina.ha.session.DeltaRequest$AttributeInfo.writeExternal(DeltaRequest.java:401)
    at org.apache.catalina.ha.session.DeltaRequest.writeExternal(DeltaRequest.java:294)
    at org.apache.catalina.ha.session.DeltaRequest.serialize(DeltaRequest.java:308)
    at org.apache.catalina.ha.session.DeltaManager.serializeDeltaRequest(DeltaManager.java:585)
    at org.apache.catalina.ha.session.DeltaManager.requestCompleted(DeltaManager.java:966)
    at org.apache.catalina.ha.session.DeltaManager.requestCompleted(DeltaManager.java:933)
    at org.apache.catalina.ha.tcp.ReplicationValve.send(ReplicationValve.java:525)
    at org.apache.catalina.ha.tcp.ReplicationValve.sendMessage(ReplicationValve.java:513)
    at org.apache.catalina.ha.tcp.ReplicationValve.sendSessionReplicationMessage(ReplicationValve.java:495)
    at org.apache.catalina.ha.tcp.ReplicationValve.sendReplicationMessage(ReplicationValve.java:406)
    at org.apache.catalina.ha.tcp.ReplicationValve.invoke(ReplicationValve.java:329)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:502)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1132)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:684)
    at org.apache.tomcat.util.net.AprEndpoint$SocketWithOptionsProcessor.run(AprEndpoint.java:2458)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
ebruchez commented 7 years ago

New problem: I do manage one move from one server to the other. However if the session is moved back to the first server, I am getting an error:

Unable to retrieve XForms engine state. Unable to process incoming request.
ebruchez commented 7 years ago

It is as if, during cache bootstrap, the content was not obtained. Yet RMICacheManagerPeerListener appears to find its peer.

ebruchez commented 7 years ago

Adding this is necessary:

<bootstrapCacheLoaderFactory
    class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"
    properties="bootstrapAsynchronously=false" />
ebruchez commented 7 years ago

It would be great if we could write an automated test for this. This might justify the use of Docker instances. Here is an example test plan:

This can be extended to launching more than 2 instances and more clients.

ebruchez commented 7 years ago

Now we need to figure out how to run this, because there is interleaving of code on the server-side (launching/stopping containers) and code on the client-side (loading pages).

ebruchez commented 7 years ago

So there will be some code, somewhere, running the test above, most likely on the client. If on the client, it will have to remote-control the start/stop of containers.

This calls for having a small server which the client can remote control to start containers. I see these possibilities:

  1. A simple servlet, created with an annotation, which is added as JAR/classes to the exploded orbeon-war. This means that at least one container with Orbeon Forms must be started before this works. Also, this requires the entire orbeon-war to be ready.
  2. A separate server run strictly for the purpose of tests. This could be based on Undertow, or Docker, but it would be started/stopped once from sbt.

I think that option 2 would be cleaner. It is a single server/remote control which only needs to be started once and is independent from the specific tests that need to run.

ebruchez commented 7 years ago

So we implemented option 2 above for tests, but now thinking that we should use Node.js's child_process directly instead.

ebruchez commented 7 years ago

Current status is that we now have a test able to start/stop containers, with HAProxy, load forms and change state, shut down one server, make sure that forms initially hitting the server down works, and conversely that the state goes back to the original server when it is back up, and then this also works reversed.

We documented a possibility where state might be lost if it hasn't been replicated yet and the client got an Ajax response. Possible solutions:

  1. Synchronous cache writes/replication with replicateAsynchronously=false
    • The worry here is that requests will now take longer to complete, possibly > 100 ms.
    • This will block the current thread within the limiter filter, unless we write a lot of code to handle this, as we don't yet have a nice async architecture for this.
  2. Fancy state recovery
    • If server gets Ajax request for newer state, somehow rollback the client to the previous state (send original HTML, diff to previous state) and re-apply latest events, which must have been stored somewhere.
    • Not necessarily realistic, as the transition between states might have cause side effects (e.g. "Send" button).
ebruchez commented 7 years ago

With testing locally (Docker images on the same machine), asynchronous state storing takes, from the caller's point of view, in the order of 10 ms. With replicateAsynchronously=false, using Form Builder as "form", this time climbs to 30-40 ms.

ebruchez commented 6 years ago

Documented but could benefit from diagrams.