在通过 kubernetes 部署的闪亮应用程序上使用 `server=FALSE` 时使用 `DT:replaceData()` 的替代方法

Alternatives to using `DT:replaceData()` when `server=FALSE` on shiny application deployed via kubernetes

出于各种原因,我希望能够在使用客户端处理时使用代理数据 table 和替换数据,即 DT::renderDataTable(..., server = FALSE).

上下文

我有一个闪亮的 application/dashboard 可以与数据库通信并向用户显示信息。用户能够在应用程序中填写一个表单,该表单将被添加到数据库中,然后闪亮的应用程序通过查询数据库来更新数据以获取新信息。

该应用程序目前正在使用 LoadBalancer 通过 kubernetes 进行部署,目的是根据需要使用多个副本来扩展该应用程序。该应用程序未通过 shinyproxy 运行。

注意事项

目前,当应用程序由单个副本(进程)运行 运行时,应用程序将表现得非常好并且能够使用 server=TRUE。但是,当我将 processes/replicas 的数量增加到 运行 时,除非在 renderDataTable 中指定 server=FALSE,否则数据无法呈现给用户。由于目前未知的原因,但我怀疑这可能是由于会话对 IP 不粘

虽然代码在 server = TRUE 时可以正常运行,但如果我想让多个用户使用应用程序,他们都不能共享一个进程,因为一旦建立多个连接,应用程序就会变得非常慢。因此,我可能需要使用 server=FALSE,这样每个用户都能够以非常重要的功能细节为代价查看数据(replaceData 停止工作)。该应用程序的产品所有者坚持认为此行为保持不变,因为存在的数据通常很大并且需要进行一些列排序和分页才能找到您想要查看的信息。提交表单时,如果我不使用 replaceData 并从头开始重建 table,用户之前的 table 状态将丢失。

因此,虽然我可以拆除数据table 并在 observeEvent

内重新生成数据
observeEvent(input$button, {
    ...
    output$table = renderDataTable({DT::datatable(df(), selection = 'single', callback = 
    JS("$.fn.dataTable.ext.errMode = 'none';"))}, server = FALSE)
    ...
})

这将提供一种会产生不良行为的解决方案,即使它会相应地更新 table。

可复制的例子

这将创建一个带有按钮和 table 的应用程序。 Select 上一行 table 然后点击按钮。预期的行为是 table 在所选行上更新为 'new_content'。这仅在 server=TRUE 时有效,在 server=FALSE.

时不会发生任何事情
library(shiny)
library(DT)
data(iris)

server <- function(input, output, session) {
  iris$new_col = ''
  df = reactive({iris})
  output$table = renderDataTable({
      DT::datatable(df(), selection = 'single', 
        callback = JS("$.fn.dataTable.ext.errMode = 'none';"))}, server = FALSE) # When TRUE code works fine,,,
  proxy = dataTableProxy('table')

  observeEvent(input$button, {
    # This line would be replacing the write to a db
    iris[input$table_rows_selected, 'new_col'] <- 'changed'
    # This line would be replacing the query to the db to reflect changes the user (and potentially other users have made between loading the data previously.
    df <- reactive({iris})
    proxy %>% replaceData(df(), rownames = TRUE, resetPaging = FALSE)
  })
}
    
ui <- fluidPage(
  actionButton('button', 'Press Me'),
  DT::DTOutput('table') 
)

shinyApp(ui, server)

我对 SO 进行了相当广泛的搜索,这是我能找到的最接近的问题: 然而,这实际上并没有得到回答,并提供了“它只是不起作用”的答案。

kubernetes.yaml(只有你是法师才看)

我包含了 yaml 文件,以防有一些 kubernetes 研究人员知道如何通过一些巧妙的技巧来具体解决上述问题。所描述的问题可能源于副本之间交换的会话,因此数据被错误传达,但老实说我不是 kubernetes 中最好的......如果是这样的话,我将能够在闪亮的应用程序中使用 server=TRUE 然后这个也能解决问题。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-appname
spec:
  replicas: 5
  selector:
    matchLabels:
      app: appname
  template:
    metadata:
      labels:
        app: appname
    spec:
      containers:
      - name: appname 
        securityContext:
            privileged: false
        image: appname:latest
        ports: 
        - name: http
          containerPort: 3838
---
apiVersion: v1
kind: Service
metadata:
  name: servive-appname
spec:
  ports:
  - name: http
    port: 3838
    protocol: TCP
    targetPort: 3838
  selector:
    app: appname
  type: LoadBalancer
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ingress-appname
  annotations:
    nginx.org/websocket-services: "service-appname"
spec:
  tls:
  - hosts:
    - appname.url.com
  rules:
  - host: appname.url.com
    http:
      paths:
      - path: /
        backend:
          serviceName: service-appname
          servicePort: 3838

我们可以尝试使用reactiveValues结合input$table_rows_selected的信息。 server 参数等于请求的 FALSE

library(shiny)
library(DT)
data(iris)

server <- function(input, output, session) {
  iris$new_col = ''
  df = reactiveValues(iris = iris)
  
  
  
  output$table = renderDataTable({
    DT::datatable(df$iris, selection = 'single', 
                  callback = JS("$.fn.dataTable.ext.errMode = 'none';"))}, server = FALSE) # When TRUE code works fine,,,
  
  
  
  observeEvent(input$button, {
    
    # This line would be replacing the write to a db
    df$iris[input$table_rows_selected, c('new_col')] <- 'changed!'
    
  })
}

ui <- fluidPage(
  actionButton('button', 'Press Me'),
  DT::DTOutput('table') 
)

shinyApp(ui, server)

这是一种客户端方法,建立在@jpdugo17 的答案和@TJGorrie 的初始示例之上,使用 stateSave 选项在重新渲染时保持 table 状态。 selectPageupdateSearch 可以与 dataTableProxy 一起使用 - input$table_state$order 的状态需要作为选项传递:

library(shiny)
library(DT)
data(iris)

iris$new_col <- ''

server <- function(input, output, session) {
  
  DF = reactiveValues(iris = iris)
  
  output$table <- DT::renderDataTable(expr = {
    if (is.null(isolate(input$table_state))) {
      DT::datatable(
        DF$iris,
        selection = 'single',
        callback = JS("$.fn.dataTable.ext.errMode = 'none';"),
        options = list(stateSave = TRUE)
      )
    } else {
      # print(isolate(input$table_state$order))
      DT::datatable(
        DF$iris,
        selection = 'single',
        callback = JS("$.fn.dataTable.ext.errMode = 'none';"),
        options = list(
          stateSave = TRUE,
          order = isolate(input$table_state$order),
          paging = TRUE,
          pageLength = isolate(input$table_state$length)
        )
      )
    }
  }, server = FALSE)
  
  proxy <- dataTableProxy('table')
  
  observeEvent(input$button, {
    DF$iris[input$table_rows_selected, c('new_col')] <- 'changed!'
  })

  observeEvent(DF$iris, {
    updateSearch(proxy, keywords = list(global = input$table_state$search$search, columns = NULL)) # see input$table_state$columns if needed
    selectPage(proxy, page = input$table_state$start/input$table_state$length+1)
  }, ignoreInit = TRUE, priority = -1)
}

ui <- fluidPage(
  actionButton('button', 'Press Me'),
  DT::DTOutput('table') 
)

shinyApp(ui, server)

这里是related article.

如果您使用 kubernetes/ingress-nginx,则可以使用 cookie 实现会话亲和力。

https://kubernetes.github.io/ingress-nginx/examples/affinity/cookie/

但是从你的 yaml 中,你正在使用 nginx.org 的 kubernetes-ingress,那么你可以阅读

https://github.com/nginxinc/kubernetes-ingress/blob/master/examples/session-persistence/README.md

但它仅在 NGINX Plus 中受支持。