简体   繁体   English

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

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

For various reasons I want to be able to use a proxied data table and replaceData while client side processing is being used ie DT::renderDataTable(..., server = FALSE) .由于各种原因,我希望能够在使用客户端处理时使用代理数据表和 replaceData,即DT::renderDataTable(..., server = FALSE)

Context语境

I have a shiny application/dashboard that communicates to a database and presents information to a user.我有一个 shiny 应用程序/仪表板,它与数据库通信并向用户提供信息。 The user is able to fill out a form in the application which will be added to the database and then the shiny app updates the data by making a query to the database to fetch the new information.用户可以在应用程序中填写表格,该表格将添加到数据库中,然后 shiny 应用程序通过查询数据库来更新数据以获取新信息。

The application is currently being deployed via kubernetes using a LoadBalancer with the intention to use multiple replicas to scale up the application as needed.该应用程序目前正在通过 kubernetes 使用LoadBalancer进行部署,目的是根据需要使用多个副本来扩展应用程序。 The application is not being run through shinyproxy.该应用程序没有通过 Shinyproxy 运行。

Caveats注意事项

Currently, when the application is being run by a single replica (process) the application will behave perfectly fine and is able to use server=TRUE .目前,当应用程序由单个副本(进程)运行时,应用程序将表现得非常好并且能够使用server=TRUE However when I increase the number of processes/replicas to run, the data is not able to be presented to users unless server=FALSE is specified in renderDataTable .但是,当我增加要运行的进程/副本的数量时,除非在renderDataTable中指定了server=FALSE ,否则数据将无法呈现给用户。 For a currently unknown reason but I suspect it might be due to the sessions not being sticky to IPs由于目前未知的原因,但我怀疑这可能是由于会话不粘到 IP

While the code is able to function fine when server = TRUE if I want to allow multiple users to application they all cannot share a single process as the application will become very slow once multiple connections are made.虽然代码能够在server = TRUE时执行 function 很好,但如果我想允许多个用户应用它们都不能共享一个进程,因为一旦建立多个连接,应用程序将变得非常慢。 As a result I likely need to use server=FALSE so each user is able to see the data at the cost of a very important functional detail ( replaceData stops working).因此,我可能需要使用server=FALSE以便每个用户都能够以非常重要的功能细节为代价查看数据( replaceData停止工作)。 The product owner of the application is insistent that this behaviour remains intact as the data present is often large and requires some column sorting and paging to find a piece of information you want to look at.应用程序的产品所有者坚持认为这种行为保持不变,因为存在的数据通常很大,并且需要一些列排序和分页才能找到您想要查看的信息。 And when submitting a form, if I do not use replaceData and reconstruct the table from scratch the users previous table state is lost.并且在提交表单时,如果我不使用replaceData并从头开始重建表,用户以前的表 state 就会丢失。

So while I could tear down the datatable and regenerate it within an observeEvent所以虽然我可以拆除数据表并在observeEvent中重新生成它

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

this would provide a solution that would yield unfavourable behaviour even though it will update the table accordingly.这将提供一种解决方案,即使它会相应地更新表,也会产生不利的行为。

Repoducible Example可重现的例子

This will create an application with a button and a table.这将创建一个带有按钮和表格的应用程序。 Select a row on the table and then click the button. Select 在表格上一行然后点击按钮。 The expected behaviour would be that the table updates with 'new_content' on the row that is selected.预期的行为是表格在所选行上更新为“new_content”。 This will only work when server=TRUE , nothing will happen when server=FALSE .这仅在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)

I have done a fairly extensive search on SO and this was the closest question I could find: DT Editing in Shiny application with client-side processing (server = F) throws JSON Error however this isn't actually answered and provides an answer of "it just does not work".我已经对 SO 进行了相当广泛的搜索,这是我能找到的最接近的问题: DT Editing in Shiny application with client-side processing (server = F) throws JSON Error但是这实际上并没有得到回答,并提供了“它只是不起作用”。

kubernetes.yaml (only look if you are a wizard) kubernetes.yaml(如果您是巫师,请只看)

I am including the yaml file incase there are some kubernetes boffins that know how to specifically solve the above issue with some clever trickery.我包括 yaml 文件,以防有一些 kubernetes boffins 知道如何通过一些巧妙的技巧专门解决上述问题。 The described problem might stem from sessions being swapped between replicas thus the data gets miscommunicated but I am honestly not the best at kubernetes... If that is the case and I would then be able to use server=TRUE within the shiny application then this would also solve the problem.所描述的问题可能源于在副本之间交换会话,因此数据被错误传达,但老实说,我在 kubernetes 方面并不是最好的......如果是这种情况,那么我将能够在 shiny 应用程序中使用 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

We can try to use reactiveValues combined with the information of input$table_rows_selected .我们可以尝试结合input$table_rows_selected的信息使用reactiveValues The server argument is equal to FALSE as requested.根据请求, 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)

在此处输入图像描述

Here is a client-side approach, building up on @jpdugo17's answer and @TJGorrie's initial example, using the stateSave option to maintain the table state on re-rendering.这是一种客户端方法,基于@jpdugo17 的答案和@TJGorrie 的初始示例,使用stateSave选项在重新渲染时维护表 state。 selectPage and updateSearch can be used along with dataTableProxy - the state of input$table_state$order needs to be passed as an option: selectPageupdateSearch可以与dataTableProxy一起使用 - input$table_state$order的 state 需要作为选项传递:

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, {
    selectPage(proxy, page = input$table_state$start/input$table_state$length+1)
    updateSearch(proxy, keywords = list(global = input$table_state$search$search, columns = NULL)) # see input$table_state$columns if needed
  }, ignoreInit = TRUE, priority = -1)
}

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

shinyApp(ui, server)

Here is a related article .这是一篇相关文章

You can achieve session affinity using cookies if you are using kubernetes/ingress-nginx.如果您使用的是 kubernetes/ingress-nginx,则可以使用 cookies 实现 session 亲和力。

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

but from your yaml, you are using nginx.org's kubernetes-ingress, then you can read但是从您的 yaml 中,您正在使用 nginx.org 的 kubernetes-ingress,然后您可以阅读

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

but it's supported only in NGINX Plus.但仅在 NGINX Plus 中支持。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM