Multi-Tenancy with separate database schemas in Activiti

One feature request often heard in the past is that of running the Activiti engine in a multi-tenant way where the data of a tenant is isolated from the others. Certainly in certain cloud/SaaS environments this is a must.

A couple of months ago I was approached by Raphael Gielen, who is a student at the university of Bonn, working on a master thesis about multi-tenancy in Activiti. We got together in a co-working coffee bar a couple of weeks ago, bounced ideas and hacked together a first prototype with database schema isolation for tenants. Very fun :-).

Anyway, we’ve been refining and polishing that code and committed it to the Activiti codebase. Let’s have a look at the existing ways of doing multi-tenancy with Activiti in the first two sections below. In the third section, we’ll dive into the new multi-tenant multi-schema feature sprinkled with some real-working code examples!

Shared Database Multi-tenancy

Activiti has been multi-tenant capable for a while now (since version 5.15). The approach taken was that of a shared database: there is one (or more) Activiti engines and they all go to the same database. Each entry in the database table has a tenant identifier, which is best to be understood as a sort of tag for that data. The Activiti engine and API’s then read and use that tenant identifier to perform its various operations in the context of a tenant.

For example, as shown in the picture below, two different tenants can have a process definition with the same key. The engine and API’s make sure there is no mixup of data.

Screenshot 2015-10-06 12.57.00

The benefit of this approach is the simplicity of deployment, as there is no difference from setting up a ‘regular’ Activiti engine. The downside is that you have to remember to use the right API calls (i.e. those that take in account the tenant identifier). Also, it has the same problem as any system with shared resources: there will always be competition for the resources between tenants. In most use cases this is fine, but there are use cases that can’t be done in this way, like giving certain tenants more or less system resources.

Multi-Engine Multi-Tenancy

Another approach, which has been possible since the very first version of Activiti is simply having one engine instance for each tenant:

Screenshot 2015-10-06 13.12.56

In this setup, each tenant can have different resource configurations or even run on different physical servers. Each engine in this picture here can of course be multiple engines for more performance/failover/etc. The benefit now is that the resources are tailored for the tenant. The downside is the more complex setup (multiple database schemas, having a different configuration file for each tenant, etc.). Each engine instance will take up memory (but that’s very low with Activiti). Also, you’d need t write some routing component that knows somehow the current tenant context and routes to the correct engine.

Screenshot 2015-10-06 13.18.36

Multi-Schema Multi-Tenancy

The latest addition to the Activiti multi-tenancy story was added two weeks ago (here’s the commit), simultaneously on version 5 and 6. Here, there is a database (schema) for each tenant, but only one engine instance. Again, in practice there might be multiple instances for performance/failover/etc., but the concept is the same:

Screenshot 2015-10-06 13.41.20

The benefit is obvious: there is but one engine instance to manage and configure and the API’s are exactly the same as with a non-multi-tenant engine. But foremost, the data of a tenant is completely separated from the data of other tenants. The downside (similar to the multi-engine multi-tenant approach) is that someone needs to manage and configure different databases. But the complex engine management is gone.

The commit I linked to above also contains a unit test showing how the Multi-Schema Multi-Tenant engine works.

Building the process engine is easy, as there is a MultiSchemaMultiTenantProcessEngineConfiguration that abstracts away most of the details:


config = new MultiSchemaMultiTenantProcessEngineConfiguration(tenantInfoHolder);

config.setDatabaseType(MultiSchemaMultiTenantProcessEngineConfiguration.DATABASE_TYPE_H2);
config.setDatabaseSchemaUpdate(MultiSchemaMultiTenantProcessEngineConfiguration.DB_SCHEMA_UPDATE_DROP_CREATE);
    
config.registerTenant("alfresco", createDataSource("jdbc:h2:mem:activiti-mt-alfresco;DB_CLOSE_DELAY=1000", "sa", ""));
config.registerTenant("acme", createDataSource("jdbc:h2:mem:activiti-mt-acme;DB_CLOSE_DELAY=1000", "sa", ""));
config.registerTenant("starkindustries", createDataSource("jdbc:h2:mem:activiti-mt-stark;DB_CLOSE_DELAY=1000", "sa", ""));
    
processEngine = config.buildProcessEngine();

This looks quite similar to booting up a regular Activiti process engine instance. The main difference is that we’re registring tenants with the engine. Each tenant needs to be added with its unique tenant identifier and Datasource implementation. The datasource implementation of course needs to have its own connection pooling. This means you can effectively give certain tenants different connection pool configuration depending on their use case. The Activiti engine will make sure each database schema has been either created or validated to be correct.

The magic to make this all work is the TenantAwareDataSourceThis is a javax.sql.DataSource implementation that delegates to the correct datasource depending on the current tenant identifier. The idea of this class was heavily influenced by Spring’s AbstractRoutingDataSource (standing on the shoulders of other open-source projects!).

The routing to the correct datasource is being done by getting the current tenant identifier from the TenantInfoHolder instance. As you can see in the code snippet above, this is also a mandatory argument when constructing a MultiSchemaMultiTenantProcessEngineConfiguration. The TenantInfoHolder is an interface you need to implement, depending on how users and tenants are managed in your environment. Typically you’d use a ThreadLocal to store the current user/tenant information (much like Spring Security does) that gets filled by some security filter. This class effectively acts as the routing component’ in the picture below:

Screenshot 2015-10-06 13.53.13

In the unit test example, we use indeed a ThreadLocal to store the current tenant identifier, and fill it up with some demo data:

 private void setupTenantInfoHolder() {
    DummyTenantInfoHolder tenantInfoHolder = new DummyTenantInfoHolder();
    
    tenantInfoHolder.addTenant("alfresco");
    tenantInfoHolder.addUser("alfresco", "joram");
    tenantInfoHolder.addUser("alfresco", "tijs");
    tenantInfoHolder.addUser("alfresco", "paul");
    tenantInfoHolder.addUser("alfresco", "yvo");
    
    tenantInfoHolder.addTenant("acme");
    tenantInfoHolder.addUser("acme", "raphael");
    tenantInfoHolder.addUser("acme", "john");
    
    tenantInfoHolder.addTenant("starkindustries");
    tenantInfoHolder.addUser("starkindustries", "tony");
    
    this.tenantInfoHolder = tenantInfoHolder;
  }
 

We now start some process instance, while also switching the current tenant identifier. In practice, you have to imagine that multiple threads come in with requests, and they’ll set the current tenant identifier based on the logged in user:

startProcessInstances("joram");
startProcessInstances("joram");
startProcessInstances("raphael");
completeTasks("raphael");

The startProcessInstances method above will set the current user and tenant identifier and start a few process instances, using the standard Activiti API as if there was no multi tenancy at all (the completeTasks method similarly completes a few tasks).

Also pretty cool is that you can dynamically register (and delete) new tenants, by using the same method that was used when building the process engine. The Activiti engine will make sure the database schema is either created or validated.

config.registerTenant("dailyplanet", createDataSource("jdbc:h2:mem:activiti-mt-daily;DB_CLOSE_DELAY=1000", "sa", ""));

Here’s a movie showing the unit test being run and the data effectively being isolated:

Multi-Tenant Job Executor

The last piece to the puzzle is the job executor. Regular Activiti API calls ‘borrow’ the current thread to execute its operations and thus can use any user/tenant context that has been set before on the thread.

The job executor however, runs using a background threadpool and has no such context. Since the AsyncExecutor in Activiti is an interface, it isn’t hard to implement a multi-schema multi-tenant job executor. Currently, we’ve added two implementations. The first implementation is called the SharedExecutorServiceAsyncExecutor:

config.setAsyncExecutorEnabled(true);
config.setAsyncExecutorActivate(true);
config.setAsyncExecutor(new SharedExecutorServiceAsyncExecutor(tenantInfoHolder));

This implementations (as the name implies) uses one threadpool for all tenants. Each tenant does have its own job acquisition threads, but once the job is acquired, it is put on the shared threadpool. The benefit of this system is that the number of threads being used by Activiti is constrained.

The second implementation is called the ExecutorPerTenantAsyncExecutor:

config.setAsyncExecutorEnabled(true);
config.setAsyncExecutorActivate(true);
config.setAsyncExecutor(new ExecutorPerTenantAsyncExecutor(tenantInfoHolder));

As the name implies, this class acts as a ‘proxy’ AsyncExecutor. For each tenant registered, a complete default AsyncExecutor is booted. Each with its own acquisition threads and execution threadpool. The ‘proxy’ simply delegates to the right AsyncExecutor instance. The benefit of this approach is that each tenant can have a fine-grained job executor configuration, tailored towards the needs of the tenant.

Conclusion

As always, all feedback is more than welcome. Do give the multi-schema multi-tenancy a go and let us know what you think and what could be improved for the future!

18 Comments

  1. Joram Barrez October 29, 2015

    Answering here on the questions asked http://www.jorambarrez.be/blog/about/#comments

    “I was trying the multi-schema multi-tenant example provided by you and it worked well. Thanks for it. However, I believe in a SaaS environment one important use-case is that you will have multiple schemas but the same database. And the schema cannot by set in the URL string while creating datasource. It needs to be set by using the command -> set search_path to ‘schema’.

    The schema should have been set at the query execution level. Creating database per tenant is like one use case solved multi-database multi tenant. Please provide your views on the same.”

    The example above works on MySQL, multiple schema’s inside the same database. So I’m not sure I understand your remark about having to use search_path? Typically you’re able to set the schema in the jdbc url.

    “Also, how do you expect the tenant Information to be passed when starting a workflow instance. Please can you advise on this aspect. In the test case we are sharing the tenantInfoholder object in the processEngineConfiguration and while setting tenant before initiating a workflow.

    I believe typically the tenantId/UserId information will come in as request parameters if we are using Rest endpoints to initiate a workflow etc. Which I believe we can access in any filter and set in the TenantInfoholder object. But How will I be able to access the tenantInfoholder object ??”

    The tenantInfoHolder object is ‘static’, ie there is one instance for the server. It is passed on process engine creation to the process engine configuration. What you typically would do is, using for example Spring Security, set the current user/tenantId in a service that also has access to the tenantInfoHolder. Starting a process instance when this information has been set will be done in the context of the tenant id (that’s where the custom DataSource implementation kicks in).

  2. nikita November 8, 2015

    Thanks for the responses Joram. Setting the search_path was for Postgres db. To give more context ->
    http://jerodsanto.net/2011/07/building-multi-tenant-rails-apps-with-postgresql-schemas/ (3rd strategy in the blog)
    http://stackoverflow.com/questions/21562713/how-can-i-switch-postgres-schemas-on-a-per-query-basis-with-hiberante

    I am able to set and get tenant Id now for all my requests/job executors. This part is clear.

    I believe by modifying the above approach a little bit I will be able to achieve Multi-tenancy(multi-schema) for postgres as well. The part I am trying to modify is that instead of fetching tenant specific datasources in TenantAwareDataSource (getCurrentDataSource()), I return default datasource for all tenants and in a MyBatisPlugin I fetch the tenantId set in the ThreadLocal variable and SET SEARCH_PATH =’tenantspecificschema’ before running any query. I hope I am in the right direction. I am testing this approach currently.

  3. Joram Barrez November 11, 2015

    @Nikita: that makes sense and comes close to what I was thinking about when needing to have a ‘default’ database (eg to store the tenant information). If nothing is supplied -> default database, otherwise use the threadlocal one.

  4. Grzegorz January 25, 2016

    Approach described here works fine. However, I’m wondering, what should be the right way to handle transaction interceptor in multi-schema multi-tenant environment.
    Standard SprintProcessEngineConfiguration class has transactionManager property and provides meaningful implementation of createTransactionInterceptor() method, while MultiSchemaMultiTenantProcessEngineConfiguration class doesn’t have such property and it’s implementation of createTransactionInterceptor() method just returns null.

    Is this something to be done for the future work or I’m not getting this right and transaction interceptor is not needed for multi-schema multi-tenant approach?

  5. Joram Barrez January 26, 2016

    @Grzegorz: The transaction interceptor is not needed. The TenantAwareDataSource acts as a single DataSource, and the transactionmanager uses that.

    The only thing that must be made sure of is that the TenantInfoHolder is set before the datasource is used, for example in a Spring Security interceptor or something like that.

  6. haiou xiang April 14, 2016

    hello,
    We are using spring-boot with Activiti 5.19 and set up multi-schema and multi-tenancy by solution above. Here codes is how we deploy by BPMN model and start workflow.
    activitiTenantInfoHolder.setCurrentTenantId(tenantId);
    new BpmnAutoLayout(wfBuilder.getBpmnModel()).execute();

    String bpmnResourceName = “Bpmn_” + workflowInfo.getName() + “_resource.bpmn”;
    try {
    processEngine.getRepositoryService().createDeployment().addBpmnModel(bpmnResourceName, wfBuilder.getBpmnModel()).name(workflowInfo.getName()).deploy();

    } catch (Exception e) {
    theLogger.error(“ERROR: The workflow \” + wfProcess.getId() + \” is failed to deploy.”);
    e.printStackTrace();
    }
    processEngine.getRuntimeService().startProcessInstanceByKey(WorkflowProcessUtil.WORKFLOW_NAME_PREFIX + startWorkflowModel.getWorkflowId(), getProcessVariables(startWorkflowModel));
    activitiTenantInfoHolder.clearCurrentTenantId();

    In our workflow definition, we define the java delegate expression, for example “”. The workflow is successfully deployed and store in tenant database, however, the starting workflow is failed, then we got below error message.
    org.activiti.engine.ActivitiException: Unknown property used in expression: ${dataReceiveTaskDelegate}
    at org.activiti.engine.impl.el.JuelExpression.getValue(JuelExpression.java:53)
    at org.activiti.engine.impl.bpmn.behavior.ServiceTaskDelegateExpressionActivityBehavior.execute(ServiceTaskDelegateExpressionActivityBehavior.java:88)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationActivityExecute.execute(AtomicOperationActivityExecute.java:60)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionNotifyListenerStart.eventNotificationsCompleted(AtomicOperationTransitionNotifyListenerStart.java:52)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:56)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:49)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionCreateScope.execute(AtomicOperationTransitionCreateScope.java:49)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionNotifyListenerTake.execute(AtomicOperationTransitionNotifyListenerTake.java:80)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionDestroyScope.execute(AtomicOperationTransitionDestroyScope.java:116)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionNotifyListenerEnd.eventNotificationsCompleted(AtomicOperationTransitionNotifyListenerEnd.java:35)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:56)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:49)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.take(ExecutionEntity.java:452)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.take(ExecutionEntity.java:430)
    at org.activiti.engine.impl.bpmn.behavior.BpmnActivityBehavior.performOutgoingBehavior(BpmnActivityBehavior.java:140)
    at org.activiti.engine.impl.bpmn.behavior.BpmnActivityBehavior.performDefaultOutgoingBehavior(BpmnActivityBehavior.java:66)
    at org.activiti.engine.impl.bpmn.behavior.FlowNodeActivityBehavior.leave(FlowNodeActivityBehavior.java:44)
    at org.activiti.engine.impl.bpmn.behavior.FlowNodeActivityBehavior.execute(FlowNodeActivityBehavior.java:36)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationActivityExecute.execute(AtomicOperationActivityExecute.java:60)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationActivityStart.eventNotificationsCompleted(AtomicOperationActivityStart.java:26)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:56)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.executeActivity(ExecutionEntity.java:457)
    at org.activiti.engine.impl.bpmn.behavior.SubProcessActivityBehavior.execute(SubProcessActivityBehavior.java:48)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationActivityExecute.execute(AtomicOperationActivityExecute.java:60)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionNotifyListenerStart.eventNotificationsCompleted(AtomicOperationTransitionNotifyListenerStart.java:52)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:56)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:49)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionCreateScope.execute(AtomicOperationTransitionCreateScope.java:49)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionNotifyListenerTake.execute(AtomicOperationTransitionNotifyListenerTake.java:80)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionDestroyScope.execute(AtomicOperationTransitionDestroyScope.java:116)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionNotifyListenerEnd.eventNotificationsCompleted(AtomicOperationTransitionNotifyListenerEnd.java:35)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:56)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:49)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.take(ExecutionEntity.java:452)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.take(ExecutionEntity.java:430)
    at org.activiti.engine.impl.bpmn.behavior.BpmnActivityBehavior.performOutgoingBehavior(BpmnActivityBehavior.java:140)
    at org.activiti.engine.impl.bpmn.behavior.BpmnActivityBehavior.performDefaultOutgoingBehavior(BpmnActivityBehavior.java:66)
    at org.activiti.engine.impl.bpmn.behavior.FlowNodeActivityBehavior.leave(FlowNodeActivityBehavior.java:44)
    at org.activiti.engine.impl.bpmn.behavior.FlowNodeActivityBehavior.execute(FlowNodeActivityBehavior.java:36)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationActivityExecute.execute(AtomicOperationActivityExecute.java:60)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationProcessStartInitial.eventNotificationsCompleted(AtomicOperationProcessStartInitial.java:45)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:56)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.pvm.runtime.AtomicOperationProcessStart.eventNotificationsCompleted(AtomicOperationProcessStart.java:64)
    at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:56)
    at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:633)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:628)
    at org.activiti.engine.impl.persistence.entity.ExecutionEntity.start(ExecutionEntity.java:380)
    at org.activiti.engine.impl.cmd.StartProcessInstanceCmd.execute(StartProcessInstanceCmd.java:110)
    at org.activiti.engine.impl.cmd.StartProcessInstanceCmd.execute(StartProcessInstanceCmd.java:37)
    at org.activiti.engine.impl.interceptor.CommandInvoker.execute(CommandInvoker.java:24)
    at org.activiti.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:57)
    at org.activiti.engine.impl.interceptor.LogInterceptor.execute(LogInterceptor.java:31)
    at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:40)
    at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:35)
    at org.activiti.engine.impl.RuntimeServiceImpl.startProcessInstanceByKey(RuntimeServiceImpl.java:77)
    at com.revitas.flex.workflow.service.WorkflowService.startProcess(WorkflowService.java:182)
    at com.revitas.flex.workflow.service.WorkflowService.deployProcess(WorkflowService.java:164)
    at com.revitas.flex.workflow.service.WorkflowService.deployStartProcess(WorkflowService.java:106)
    at com.revitas.flex.workflow.rest.WorkflowController.deployStartWorkflowProcess(WorkflowController.java:30)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:221)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:776)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:705)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:869)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:648)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration$ApplicationContextHeaderFilter.doFilterInternal(EndpointWebMvcAutoConfiguration.java:295)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.java:102)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:186)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:119)
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:65)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:85)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:68)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
    at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:676)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:142)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:518)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1091)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:668)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1521)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1478)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
    Caused by: org.activiti.engine.impl.javax.el.PropertyNotFoundException: Cannot resolve identifier ‘dataReceiveTaskDelegate’
    at org.activiti.engine.impl.juel.AstIdentifier.eval(AstIdentifier.java:83)
    at org.activiti.engine.impl.juel.AstEval.eval(AstEval.java:50)
    at org.activiti.engine.impl.juel.AstNode.getValue(AstNode.java:26)
    at org.activiti.engine.impl.juel.TreeValueExpression.getValue(TreeValueExpression.java:114)
    at org.activiti.engine.impl.delegate.ExpressionGetInvocation.invoke(ExpressionGetInvocation.java:33)
    at org.activiti.engine.impl.delegate.DelegateInvocation.proceed(DelegateInvocation.java:37)
    at org.activiti.engine.impl.delegate.DefaultDelegateInterceptor.handleInvocation(DefaultDelegateInterceptor.java:25)
    at org.activiti.engine.impl.el.JuelExpression.getValue(JuelExpression.java:48)
    … 185 more

    Our workflow works fine on activity default multi-tenancy(Shared database). Does multi-schema and multi-tenancy works for spring boot? it seems our java delegate class not found by activiti context.

    thanks,
    –Haiou

  7. Joram Barrez April 15, 2016

    The classpath is shared, even in this setup .. so not sure what could go wrong here. There is nothing in the multi-schema multi-tenant setup that could influence this.
    Can you put your failing example somewhere on github so we can have a look?

  8. haiou xiang April 15, 2016

    Sorry, I cannot post our code on github because of company policy. In addition, I found out the activiti-spring boot use configuration class ‘org.activiti.spring.SpringProcessEngineConfiguration’ which has property ‘protected ApplicationContext applicationContext;’, once the activiti-spring boot server started, ‘applicationContext’ has all spring beans including our java delegate classes.
    So I plan to implement a new processEngineConfiguration class to have both ‘MultiSchemaMultiTenantProcessEngineConfiguration’ and ‘SpringProcessEngineConfiguration’ codes, hopefully it will work.

    Any suggestion for our approach or different solution?

  9. haiou xiang April 18, 2016

    I figured out the solution which invokes two more methods setExpressionManager and setBeans in MultiSchemaMultiTenantProcessEngineConfiguration, then the Spring bean context maps to expression manager. For some reason, activiti – Springboot doesn’t work for MultiSchemaMultiTenantProcessEngineConfiguration, I have to manually set those two properties.

    config.setExpressionManager(new SpringExpressionManager(applicationContext,config.getBeans()));
    config.setBeans(new SpringBeanFactoryProxyMap(applicationContext));

  10. Joram Barrez April 18, 2016

    “For some reason, activiti – Springboot doesn’t work for MultiSchemaMultiTenantProcessEngineConfiguration”

    No, that’s correct. We didn’t make the MSMT config compatible with the Spring engine config.
    But it seems you are on the right track!

  11. Dan_ June 13, 2016

    Hi Joram,
    In the example above, it seems that every time an interaction with engine such as deploy or start a process, then it has to be preceeded by tenantInfoHoler.setCurrentTenantId(tenantid) and succeeded by tenantInfoHoler.clearCurrentTenantId(…).

    I am wondering how the engine behaves when multiple threads interact concurrently with engine by setting and clearing the currentTenantId? This case seems problematic unless access to tenantInfoHoler set and clear currentTenantId is serialized. Am I right, or I have missed something?

    Thanks,

  12. Joram Barrez June 13, 2016

    @Dan_: You are correct. I hinted at that in the blog above with the sentence ” Typically you’d use a ThreadLocal to store the current user/tenant information (much like Spring Security does) that gets filled by some security filter. “.

    More concretely, this means that your user identification, let’s say an email or userId, needs to be able to be used to determine the tenantId. When a request comes in, that’s a threads and the filter kicks in extracting the information from the request needed to set the tenantId.

  13. Luca Pinelli July 5, 2016

    Hi Joram,
    I tried to set the DbIdGenerator using the following code:

    DbIdGenerator dbIdGenerator = new DbIdGenerator();
    dbIdGenerator.setIdBlockSize(config.getIdBlockSize());
    dbIdGenerator.setCommandExecutor(config.getCommandExecutor());
    dbIdGenerator.setCommandConfig(config.getDefaultCommandConfig().transactionRequiresNew());
    config.setIdGenerator(dbIdGenerator);

    but both the methods config.getCommandExecutor() and config.getDefaultCommandConfig() return null. How can I set the DbIdGenerator?

  14. Luca Pinelli July 5, 2016

    sorry I forgot an important information, I’m using the MultiSchemaMultiTenantProcessEngineConfiguration.

    Thanks,
    Luca

  15. nedumaran September 21, 2016

    we are planning to use activiti with spring boot. could you please explain how to integrate this with spring-boot? Also is it possible to have different set of tables for each tenant in the same schema? if anybody has done like that, please share the approach.

  16. Joram Barrez September 21, 2016

    @ nedumaran: haven’t tried it yet, but what you’d have to do in theory is swap the ProcessEngineConfiguration wth he MultiTenantProcessEngineConfiguration. Spring Boot will only use the default, is no other one is defined.

  17. sandip September 27, 2016

    Hi Joram,

    I was going through this new feature of activiti of having single engine with multiple schema. Can you please let us know whether below example will be supported or not.

    We are looking to use single aciviti engine server to do the job for multiple applications.

    e.g. 1 activiti engine having app1, app2 and app3 and connects to db1, db2 and db3. DB1 contains all process and business data of app1. DB2 contains process and business data of app2. DB3 contains all process and business data of app3.

    Is it supported or not? if not how to achieve it.

    Regards
    Sandip Desale

  18. Rik March 16, 2017

    Hello All,

    I am new in Alfresco Activiti. I want to implement multi tenancy by using multiple database. Right now I am not sure Multi tenant would be in same schema or in different database.

    Please guide me from where I download the source code and test it.

    I am also confused regarding the Activiti-explorer and activiti-app.

Leave a Reply

Your email address will not be published. Required fields are marked *