Grails异步+多租户问题



使用Grails 4.0.10,我已经编写了一个基于标识符的多租户应用程序的大部分内容。现在的问题是实现"看门人"。这是一个预定的服务,它扫描数据库中超过截止日期的文档,并分配一个"过期"。向他们陈述。

Document是一个多租户域。看门人应该跨租户工作。

计划服务生成一个异步操作。使用Tenants.withoutId查找所有候选文档。

问题出现在更新数据库时。即使逻辑被封闭在Tenants.withId中,操作也以TenantNotFoundException结束。没有找到租户,事务回滚。

在提交事务时发生。多个save()操作和审计日志已经完成,没有问题。

了解来源:Document域关联了其他域:Assignment(plain)、AsmProgress(multi-tenant)、DocAction(multi-tenant)。对于给定的文档,所有这些都属于同一个承租者。logRecordService为审计服务

这是关键的源代码。

def forceExpireMultiple(InsUser actor, Map docActions) {
if (log.debugEnabled) log.debug "FORCE_EXPIRE_MULTIPLE_S << ${actor}, ${docActions?.keySet()}"
docActions.each {entry ->
String caseId = entry.key
DocAction docAction = entry.value
// Each doc action is expired in its own transaction.
DocAction.withTransaction {status ->
if (log.traceEnabled) log.trace "forceExpireMultiple.ttag: ${docAction.ttag}"
Tenants.withId(docAction.ttag) {
docAction = get(docAction.id)
doForceExpire(actor, caseId, docAction)
}
}
}
}
/**
* Expire a single doc action with its assignments and document.
*/
private doForceExpire(InsUser actor, String caseId, DocAction docAction) {
if (log.debugEnabled) log.debug "DO_FORCE_EXPIRE_S << ${caseId}, ${docAction}"
final String ttag = docAction.ttag
// Terminate all assignments.
docAction.assignments.each {assignment ->
def progress = AsmProgress.of(assignment, ProgressType.EXPIRED)
progress.ttag = ttag
progress.save(failOnError: true)
// The assignment is now done.
assignment.stage = AsmState.FINISHED
assignment.save(failOnError: true)
logRecordService.logExpire(assignment, actor)
}
// Expire document.
Document document = docAction.document
document.expire()
document.save(failOnError: true)
logRecordService.logExpire(document, actor)
// Expire the doc action itself.
docAction.expire()
docAction.save(failOnError: true)
logRecordService.logExpire(docAction, actor)
}

下面是一些调试和堆栈跟踪输出,为了可读性进行了编辑。

[pool-1-thread-1] se.insignia.web.DocActionService: FORCE_EXPIRE_MULTIPLE_S << [User global::policy], [FDH176]
[pool-1-thread-1] se.insignia.web.DocActionService: forceExpireMultiple.ttag: acme
[pool-1-thread-1] se.insignia.web.DocActionService: DO_FORCE_EXPIRE_S << FDH176, FDH176/acme(ACTIVE): ... (Acme Industries Ltd.)
[pool-1-thread-2] se.insignia.web.log.LogRecordService: Logging Assignment with tenantId [acme]
[pool-1-thread-3] se.insignia.web.log.LogRecordService: Logging Document with tenantId [acme]
[pool-1-thread-4] se.insignia.web.log.LogRecordService: Logging DocAction with tenantId [acme]
org.grails.datastore.mapping.multitenancy.exceptions.TenantNotFoundException: Tenant could not be resolved outside a web request
at grails.gorm.multitenancy.Tenants.currentId(Tenants.groovy:73)
at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:100)
at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:453)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
at se.insignia.web.DocAction.withTransaction(DocAction.groovy)
at se.insignia.web.DocActionService$_forceExpireMultiple_closure6.doCall(DocActionService.groovy:262)
[This is withTransaction in forceExpireMultiple]
at se.insignia.web.DocActionService.forceExpireMultiple(DocActionService.groovy:258)

从调试输出中可以看到,在异常发生之前,所有doForceExpire都执行了,包括审计日志记录。

根本原因是什么,我怎样才能找到它?显然,我自己的代码之外的一些假设被违反了。它们是什么?

我尝试了很多代码的变体,包括注释掉所有的审计日志。

在没有解决问题的答案和理论的情况下,我选择了这篇文章中描述的解决方案。这个想法是将租户id存储在线程本地存储中。

public class InsigniaExplicitTenantContext {
private static final ThreadLocal<Serializable> CONTEXT = new ThreadLocal<>();

public static void setTenantId(Serializable tenantId) {
CONTEXT.set(tenantId);
}
public static Serializable getTenantId() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}

这意味着修改租户解析器。通常,它从HTTP请求推断租户。现在,如果没有web请求,它会检查线程本地存储

为方便起见,我还添加了一个模拟Tenants.withId方法的类:

class WithTenant {
/**
* Works as Tenants.withId but additionally stores the tenant id
* in thread local storage.
*/
static <T> T id(Serializable tenantId, Closure<T> callable) {
Tenants.withId(tenantId) {
InsigniaExplicitTenantContext.setTenantId(tenantId)
callable.call()
}
}
}

该解决方案仅在明显没有web请求的计划作业中需要。所以它相当干净。

然而,原始问题的答案仍然受欢迎。我只是不能再等了。

最新更新