【问题标题】:Grails Async + Multi-Tenant WoesGrails 异步 + 多租户问题
【发布时间】:2021-10-31 22:56:18
【问题描述】:

使用 Grails 4.0.10 我编写了一个基于鉴别器的多租户应用程序的大部分工作。现在的问题是实施“看门人”。这是一项计划服务,它会扫描数据库中超过截止日期的文档,并为它们分配“过期”状态。

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

计划的服务产生一个异步操作。使用 Tenants.withoutId 查找所有候选文档按预期工作。

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

这发生在提交事务时。多个save() 操作和审计日志此时已顺利完成。

了解来源:Document 域与其他域相关联:Assignment(普通)、AsmProgress(多租户)、DocAction(多租户)。对于给定的文档,所有这些都属于同一个租户。 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 都在异常之前执行,包括审计日志记录。

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

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

【问题讨论】:

  • 您使用的是哪个租户解析器?看起来您可能正在使用需要主动请求的请求。
  • 对,一个子域租户解析器,仿照 doco 中的那个。是的,它的正常模式是对 Web 请求进行操作。但也有需要访问其他租户的监督角色。到目前为止,“Tenants.withId”已经完成了这项工作。

标签: hibernate grails grails-orm


【解决方案1】:

在没有解决问题的答案和理论的情况下,我选择了解决方案described in this post。想法是将租户 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 请求的计划作业中才需要解决方法。所以它相当干净。

但是,仍然欢迎回答原始问题。只是我已经等不及了。

【讨论】:

    猜你喜欢
    • 2021-12-16
    • 2021-07-04
    • 2015-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多