Deschideți o dată, închideți automat: un model de conexiune la resurse pentru R

URMĂREȘTE-NE
16,065FaniÎmi place
1,142CititoriConectați-vă

Conexiunile suspendate la baze de date cauzează rareori scurgeri imediate de date, dar sunt o cauză comună a întreruperilor, a performanței degradate și a incidentelor de disponibilitate. În sistemele reglementate sau din sectorul public, eșecurile de disponibilitate pot avea consecințe legale și contractuale. Prin urmare, gestionarea corectă a conexiunilor nu este doar o preocupare tehnică, ci și o preocupare de fiabilitate.

GitHub reveals database infrastructure was the villain behind February spate of outages. Again.

Cel de-al treilea incident din 25 februarie a implicat din nou ProxySQL, când „conexiunile active la baze de date au depășit un prag critic care a schimbat comportamentul acestei noi infrastructuri. Deoarece conexiunile au rămas peste pragul critic după remediere, sistemul a revenit într-o stare degradată”.

Aceste tipuri de eșecuri pot părea operaționale, dar în mediile de producție pot declanșa încălcări ale SLA, impact asupra clienților și risc potențial de reglementare – în special acolo unde disponibilitatea și rezistența fac parte din cerințele de conformitate (de exemplu, articolul 32 din GDPR pentru sistemele de prelucrare a datelor cu caracter personal). Gestionarea corectă a conexiunilor – deschiderea lor numai atunci când este necesar și asigurarea curățării deterministe – nu este, prin urmare, doar „bună igienă” în programarea R, ci un aspect practic al construirii de sisteme robuste și fiabile.

Aceste riscuri sunt relevante și în ecosistemul R, unde accesul la bazele de date este obișnuit în procesele de lungă durată, cum ar fi aplicațiile Shiny, API-urile instalatorilor și conductele de date programate. În timp ce R este adesea asociat cu analiza academică, este utilizat pe scară largă în sistemele de producție din guvern și industrie. În aplicațiile Shiny, de exemplu, fiecare sesiune de utilizator poate deschide conexiuni la baze de date, iar conexiunile prost gestionate pot acumula rapid și epuiza resursele backend. Deoarece aceste aplicații sunt de obicei de lungă durată, micile scurgeri de conexiune pot degrada performanța în tăcere sau pot cauza întreruperi. Prin urmare, gestionarea corectă a conexiunilor nu este doar o bună practică în R, ci este esențială pentru construirea de aplicații fiabile, scalabile și pregătite pentru producție.

Această postare introduce un model folosind în jur on.exit() care face conexiunile sigure, automate și fără griji.

Din documentația R a funcției on.exit:

on.exit înregistrează expresia dată ca argument ca fiind necesară pentru a fi executată atunci când funcția curentă iese (fie natural, fie ca rezultat al unei erori). Acest lucru este util pentru resetarea parametrilor grafici sau pentru efectuarea altor acțiuni de curățare.

Aceasta este o caracteristică versatilă a R-funcitons și o putem folosi pentru a construi o funcție de conexiune care va asigura că conexiunea este închisă din nou. În esență, dorim să construim o funcție care poate face ceva de genul acesta:

get_data <- function() {
  connect()
  DBI::dbGetQuery(...)
}

Pentru ca acest lucru să funcționeze, connect() trebuie să înregistreze expresia R care va închide conexiunea deschisă când este funcția de apelare ieșiri, în limbajul R aceasta se numește cadru părinte și (nesurprinzător) poate fi preluat prin intermediul funcției parent.frame(). Echipat cu aceste două funcții putem scrie acum connect() funcţie:

connect <- function() {
  caller <- parent.frame()
  # If connection already exists in this frame, reuse it
  if (exists(".db", envir = caller, inherits = FALSE)) {
    return(get(".db", envir = caller))
  }
  # Create new connection
  db <- dbConnect(odbc::odbc(), "mot")
  # Store it in the caller's frame
  assign(".db", db, envir = caller)
  # Ensure cleanup when the calling function exits
  do.call(
    what = on.exit,
    args = list(
      quote({
        if (exists(".db", inherits = FALSE)) {
          dbDisconnect(.db)
          rm(.db)
        }
      }),
      add = TRUE
    ),
    envir = caller
  )
  db
}

Rețineți, de asemenea, în definiția funcției de mai sus că, pentru a putea reutiliza o conexiune deschisă, obiectul de conexiune (db) trebuie depozitat undeva. Returnarea acestuia ca rezultat al funcției nu este o garanție că va fi atribuită unui simbol. Acest lucru este problematic deoarece tot ceea ce nu este legat de un simbol într-o sesiune R este supus colectării de gunoi a lui R. De aceea, în plus, este atribuit și unei variabile numite .db în cadrul de apelare.

O problemă cu această implementare este că este vulnabilă pentru a ciocnire de nume: Nu știm dacă ar exista deja un simbol în cadrul de apelare care – poate întâmplător – are același nume .dbdar reprezintă poate ceva cu totul diferit.

Pentru a remedia acest lucru, putem crea în mod explicit un închidere adică o funcție care poartă propriul său mediu. Pentru mai multe detalii vezi explicația lui Hadley Wickham aici.

Vă ofer aici un exemplu de jucărie, în care obiectul nostru de conexiune este reprezentat de un șir aleator de 3 litere. Setarea acestei variabile la NULL este menită să reprezinte deconectarea de la resursă. Puteți înlocui aceste linii cu codul dvs. pentru conectarea și deconectarea efectivă de la resursă.

connect <- local({
  # Private environment, created once
  state <- new.env(parent = emptyenv())
  function() {
    caller <- parent.frame()
    if (identical(caller, .GlobalEnv)) {
      warning("connect() was called from Global Environment, cannot perform automatic disconnect")
    }

    # If connection already exists, reuse it
    if (exists("db", envir = state, inherits = FALSE)) {
      cat("connection already exists", state$db, 'n')
    } else { 
      # Create connection
      state$db <- paste0("db_", paste0(sample(letters, 3), collapse = ''))
      # Ensure cleanup when the *calling* function exits
      cleanup <- function() {
        if (exists("db", envir = state)) {
          # dbDisconnect(state$db)
          cat("disconnecting", state$db, 'n')
          state$db <- NULL
          rm("db", envir = state)
        }
      }
      # do.call(on.exit, list(quote(cleanup()), add = TRUE), envir = caller)
      do.call(on.exit, list(
      substitute(FUN(), list(FUN = cleanup)), add = TRUE), envir = caller)
      cat("new connection", state$db, 'n')
    }
  }
})

foo <- function() {
  cat("inside foon")
  connect()
  bar()
}

bar <- function() {
  cat("inside barn")
  goo()
}

goo <- function() {
  cat("inside goon")
  connect()
}

foo()
## inside foo
## new connection db_kpt 
## inside bar
## inside goo
## connection already exists db_kpt 
## disconnecting db_kpt
goo()
## inside goo
## new connection db_npc 
## disconnecting db_npc

Acest model folosește o închidere pentru a menține starea conexiunii și on.exit() pentru a înregistra logica de curățare în cadrul apelantului. Aplicarea substitute() pentru funcția de curățare este importantă, deoarece definiția funcției noastre trăiește numai în închiderea creată, nu în cadrul părintelui. Ce substitute() face este să înlocuiți simbolul (FUN) de către funcțiile noastre de curățare arborele de analiză neevaluat. Aceasta în sine este din nou o închidere (o funcție plus un mediu atașat) care păstrează accesul la starea sa privată chiar și atunci când este evaluată ulterior. Rezultatul este un manager de conexiune sigur, idempotent, care funcționează transparent în apelurile de funcții imbricate, fără a avea o stare de scurgere sau a necesita demontare explicită.

Fără stare globală, fără ciocniri de nume și nicio povară pentru apelant.

Nota că, când connect() este rulat în Global Environmentexpresia prevăzută la on.exit() va evalua și este responsabilitatea apelantului să închidă manual conexiunea.

connect()
new connection db_ykc
Warning:
In connect() : connect() was called from Global Environment, cannot perform automatic disconnect

Dominic Botezariu
Dominic Botezariuhttps://www.noobz.ro/
Creator de site și redactor-șef.

Cele mai noi știri

Pe același subiect

LĂSAȚI UN MESAJ

Vă rugăm să introduceți comentariul dvs.!
Introduceți aici numele dvs.