You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Shiny异步编程使用{crew}遇维度不匹配错误求助

Shiny异步绘图并发问题分析与优化建议

需求场景

Shiny应用教师端支持通过selectizeInput选择学生ID,从Firestore获取数据并绘制ggiraph交互式图表,图表点可点击弹出模态框展示详情。

面临挑战

10-15位教师同时使用,单学生图表绘制耗时≥2秒,常规Shiny并发体验差,期望通过{crew}在独立R会话中异步绘图,实现主应用会话+独立绘图会话的架构。

错误问题

并发测试5-8用户时出现以下错误,导致部分用户断开连接:

Warning: Error in attributes<-: dims [product 5] do not match the length of object [1]
63: query_status
62: mirai::status
61: <Anonymous>
57: daemons_info
54: private$.launcher$scale
53: .subset2(self, "scale")
52: controller()$pop
51: observe
50: <observer>
3: runApp
2: print.shiny.appobj
1: <Anonymous>

Warning: Error in attributes<-: dims [product 5] do not match the length of object [1]
59: query_status
58: mirai::status
57: <Anonymous>
53: daemons_info
52: controller()$client$summary
51: observe
50: <observer>
3: runApp
2: print.shiny.appobj
1: <Anonymous>

Warning: Error in attributes<-: dims [product 5] do not match the length of object [1]
63: query_status
62: mirai::status
61: <Anonymous>
57: daemons_info
55: self$tally
54: private$.launcher$scale
53: .subset2(self, "scale")
52: controller()$pop
51: observe
50: <observer>
3: runApp
2: print.shiny.appobj
1: <Anonymous>

代码问题诊断

你的{crew}实现存在以下核心问题:

1. 每个用户会话创建独立crew控制器

在模块内为每个用户实例化crew_controller_local(workers=1),10-15个用户会创建10-15个独立控制器,每个控制器至少启动1个worker进程。过多的控制器和进程会导致系统资源竞争,mirai底层进程通信时出现维度不匹配的错误(即你看到的dims [product 5] do not match)。

2. 全局变量导致控制器实例混乱

使用curr_controller <<-全局赋值控制器,在Shiny多用户环境中,不同用户的模块会覆盖这个全局变量,导致:

  • 一个用户终止会话时错误关闭其他用户的控制器
  • 查询任务状态时访问错误的控制器实例,引发状态查询异常

3. 重复任务推送与队列积压

reactivePoll每10秒触发一次,每次都会检查缓存并推送新任务,即使队列中已有相同学生ID+数据哈希的任务。任务积压会导致控制器状态查询时出现数据维度不匹配的错误。

4. 任务command参数传递错误

推送任务时,command参数直接传入了Shiny reactive对象(event_names_list_stu、accessToken、uid),虽然data参数里传了对应的值,但command中应该引用data里的变量,而非reactive对象本身,可能导致任务执行时上下文错误。

优化方案建议

1. 全局共享crew控制器

在应用启动时创建一个全局的crew控制器,所有用户共享,避免重复创建控制器和进程:

# 在app.R全局环境中初始化控制器
global_controller <- crew_controller_local(
  workers = 8,  # 根据并发数设置,建议等于预期最大用户数的60%-80%
  seconds_idle = 30  # 空闲worker自动终止的时间,避免资源浪费
)
global_controller$start()

# 应用停止时终止控制器
onStop(function() {
  global_controller$terminate()
})

2. 修正模块内控制器使用逻辑

模块内直接使用全局控制器,去掉模块内的控制器创建逻辑:

# 模块server中移除原有的controller() reactive和curr_controller变量
# 直接使用global_controller

observeEvent(event_names_list_stu(), {
  req(uid())
  student_id <- uid()
  data_hash <- current_data_hash()

  cached_result <- cache_manager$get_cached_plot(student_id, data_hash)
  if (!is.null(cached_result)) {
    reactive_result(cached_result)
    reactive_status(paste(format(Sys.time()), "Retrieved from cache 📋"))
    return()
  }

  # 检查队列中是否已有相同任务(避免重复推送)
  existing_tasks <- global_controller$client$summary()
  task_exists <- any(existing_tasks$description == paste(student_id, data_hash, sep = "-"))

  if (!task_exists) {
    reactive_result(NULL)
    global_controller$push(
      command = create_inst_progress_visual(event_names_list_stu, accessToken, uid),
      data = list(
        create_inst_progress_visual = create_inst_progress_visual,
        event_names_list_stu = event_names_list_stu(),
        accessToken = accessToken(),
        uid = uid()
      ),
      packages = c("ggplot2", "ggiraph", "dplyr", "httr2", "frstore", "ggimage", "here"),
      description = paste(student_id, data_hash, sep = "-")  # 添加任务标识,用于去重
    )
    reactive_poll(TRUE)
  }
})

3. 优化reactivePoll逻辑

让checkFunc返回数据的哈希值,只有当数据真正变化时才触发valueFunc,避免无意义的任务检查:

event_names_list_stu <- reactivePoll(
  10000,
  session,
  checkFunc = function() {
    # 返回数据哈希,只有哈希变化时才更新valueFunc
    data <- get_all_event_names_for_a_student(collection_paths_e, uid(), accessToken())
    digest::digest(data)
  },
  valueFunc = function() {
    get_all_event_names_for_a_student(collection_paths_e, uid(), accessToken())
  }
)

4. 任务错误捕获

在绘图函数中加入错误捕获,避免单个任务失败导致控制器状态异常:

create_inst_progress_visual <- function(event_names_list_stu, accessToken, uid) {
  tryCatch({
    # 原绘图逻辑
    list(
      progress_plot = plot,
      status = status,
      dat = dat
    )
  }, error = function(e) {
    message("绘图任务失败: ", e$message)
    list(
      progress_plot = NULL,
      status = NULL,
      dat = NULL,
      error = e$message
    )
  })
}

5. 优化任务状态查询逻辑

避免频繁调用controller()$client$summary(),减少状态查询的开销:

observe({
  req(reactive_poll())
  invalidateLater(millis = 500)  # 延长轮询间隔,减少资源消耗
  task <- global_controller$pop()

  if (!is.null(task)) {
    if ("result" %in% names(task) && !is.null(task$result)) {
      if ("progress_plot" %in% names(task$result[[1]]) && !is.null(task$result[[1]]$progress_plot)) {
        progress_map <- task$result[[1]]$progress_plot
        statuz <- task$result[[1]]$status
        datz <- task$result[[1]]$dat

        cache_manager$update_cache(uid(), progress_map, current_data_hash())
        reactive_result(progress_map)
        status(statuz)
        dat(datz)
      } else if ("error" %in% names(task$result[[1]])) {
        reactive_status(paste(format(Sys.time()), "绘图失败: ", task$result[[1]]$error))
      }
    }
  }

  # 只在有任务时更新状态
  task_count <- sum(global_controller$client$summary()$assigned) - sum(global_controller$client$summary()$complete)
  if (task_count > 0) {
    reactive_status(status_message(n = task_count))
    reactive_poll(TRUE)
  } else {
    reactive_status(paste(format(Sys.time()), "无待处理任务"))
    reactive_poll(FALSE)
  }
})

额外建议

  • 缓存机制优化:使用分布式缓存(如Redis)替代本地内存缓存,支持多进程/多实例共享缓存,避免不同worker重复绘制相同图表。
  • 监控与日志:添加任务执行日志,便于排查并发场景下的错误。
  • 资源限制:根据服务器CPU核心数设置worker数量,避免过度占用资源(一般worker数不超过CPU核心数的80%)。

内容的提问来源于stack exchange,提问作者umair durrani

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 23:14:54