Saltar al contenido

Seguridad en Hilos

Qué Es Seguro para Hilos

Los siguientes objetos están diseñados para ser compartidos de forma segura entre múltiples goroutines:

Expresión Compilada (*Expression)

Un *Expression compilado es inmutable después de su creación. Solo contiene el AST analizado y la cadena de expresión original. Puede llamar a Evaluate() o EvaluateWithOptions() desde cualquier número de goroutines simultáneamente:

// Safe: one expression, many goroutines.
expr := fhirpath.MustCompile("Patient.name.family")

var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func(resource []byte) {
        defer wg.Done()
        result, err := expr.Evaluate(resource)
        // handle result...
    }(resources[i])
}
wg.Wait()

Caché de Expresiones (*ExpressionCache)

El ExpressionCache (incluyendo el global DefaultCache) usa un sync.RWMutex internamente. Todos los métodos – Get(), MustGet(), Clear(), Size(), Stats() y HitRate() – son seguros para uso concurrente:

// Safe: concurrent cache access from multiple goroutines.
cache := fhirpath.NewExpressionCache(500)

var wg sync.WaitGroup
for _, exprStr := range expressions {
    wg.Add(1)
    go func(e string) {
        defer wg.Done()
        compiled, err := cache.Get(e)
        // use compiled...
    }(exprStr)
}
wg.Wait()

Funciones de Conveniencia

Las funciones de nivel superior EvaluateCached(), GetCached() y MustGetCached() todas delegan a DefaultCache y por lo tanto son seguras para uso concurrente.

Qué No Se Comparte

Contexto de Evaluación (eval.Context)

Cada llamada a Evaluate() o EvaluateWithOptions() crea un nuevo eval.Context internamente. El contexto contiene estado mutable de evaluación: el valor actual $this, $index, variables y límites. Nunca debe compartirse entre evaluaciones concurrentes.

Normalmente no necesita preocuparse por esto porque la API pública crea un contexto nuevo por llamada. Sin embargo, si crea un eval.Context manualmente, no lo reutilice entre goroutines:

// WRONG -- sharing a context between goroutines.
ctx := eval.NewContext(resource)
go func() { expr1.EvaluateWithContext(ctx) }() // DATA RACE
go func() { expr2.EvaluateWithContext(ctx) }() // DATA RACE
// CORRECT -- each goroutine gets its own context.
go func() {
    ctx := eval.NewContext(resource)
    expr1.EvaluateWithContext(ctx)
}()
go func() {
    ctx := eval.NewContext(resource)
    expr2.EvaluateWithContext(ctx)
}()

Slices de Bytes del Recurso

Los datos del recurso []byte pasados a Evaluate() son de solo lectura durante la evaluación. Es seguro pasar el mismo slice de bytes a múltiples evaluaciones concurrentes siempre que ninguna goroutine lo mute durante la evaluación.

Patrón de Evaluación Concurrente

El patrón concurrente más común es un fan-out donde múltiples recursos se evalúan en paralelo usando la misma expresión compilada:

package main

import (
    "fmt"
    "sync"

    "github.com/gofhir/fhirpath"
)

func main() {
    // Compile once.
    expr := fhirpath.MustCompile("Patient.name.family")

    resources := [][]byte{
        []byte(`{"resourceType":"Patient","name":[{"family":"Smith"}]}`),
        []byte(`{"resourceType":"Patient","name":[{"family":"Johnson"}]}`),
        []byte(`{"resourceType":"Patient","name":[{"family":"Williams"}]}`),
        []byte(`{"resourceType":"Patient","name":[{"family":"Brown"}]}`),
    }

    results := make([]fhirpath.Collection, len(resources))
    errors := make([]error, len(resources))

    var wg sync.WaitGroup
    for i, res := range resources {
        wg.Add(1)
        go func(idx int, resource []byte) {
            defer wg.Done()
            results[idx], errors[idx] = expr.Evaluate(resource)
        }(i, res)
    }
    wg.Wait()

    for i, result := range results {
        if errors[i] != nil {
            fmt.Printf("resource %d: error: %v\n", i, errors[i])
        } else {
            fmt.Printf("resource %d: %v\n", i, result)
        }
    }
}

Patrones de Producción

Manejador HTTP

En un servidor HTTP, cada manejador de solicitud se ejecuta en su propia goroutine. Las expresiones compiladas y la caché de expresiones pueden compartirse entre todos los manejadores:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "time"

    "github.com/gofhir/fhirpath"
)

// expressionCache is shared across all request handlers.
var expressionCache = fhirpath.NewExpressionCache(1000)

// Pre-compiled expressions for known operations.
var (
    exprFamilyName = fhirpath.MustCompile("Patient.name.family")
    exprBirthDate  = fhirpath.MustCompile("Patient.birthDate")
    exprActive     = fhirpath.MustCompile("Patient.active")
)

func handleExtract(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "failed to read body", http.StatusBadRequest)
        return
    }

    // Each evaluation creates its own internal eval.Context --
    // no shared mutable state between requests.
    family, err := exprFamilyName.EvaluateWithOptions(body,
        fhirpath.WithContext(r.Context()),
        fhirpath.WithTimeout(2*time.Second),
    )
    if err != nil {
        http.Error(w, fmt.Sprintf("evaluation error: %v", err),
            http.StatusInternalServerError)
        return
    }

    resp := map[string]interface{}{
        "familyName": family.String(),
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

func handleDynamic(w http.ResponseWriter, r *http.Request) {
    // For user-supplied expressions, use the cache.
    exprStr := r.URL.Query().Get("expression")
    if exprStr == "" {
        http.Error(w, "missing expression parameter", http.StatusBadRequest)
        return
    }

    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "failed to read body", http.StatusBadRequest)
        return
    }

    // GetCached is safe for concurrent use.
    compiled, err := expressionCache.Get(exprStr)
    if err != nil {
        http.Error(w, fmt.Sprintf("invalid expression: %v", err),
            http.StatusBadRequest)
        return
    }

    result, err := compiled.EvaluateWithOptions(body,
        fhirpath.WithContext(r.Context()),
        fhirpath.WithTimeout(2*time.Second),
        fhirpath.WithMaxCollectionSize(1000),
    )
    if err != nil {
        http.Error(w, fmt.Sprintf("evaluation error: %v", err),
            http.StatusInternalServerError)
        return
    }

    resp := map[string]interface{}{
        "result": result.String(),
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

func main() {
    http.HandleFunc("/extract", handleExtract)
    http.HandleFunc("/evaluate", handleDynamic)
    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Pool de Workers

Para procesamiento por lotes (por ejemplo, validar todos los recursos en una base de datos), use un pool de workers para limitar la concurrencia:

package main

import (
    "fmt"
    "sync"
    "time"

    "github.com/gofhir/fhirpath"
)

// ValidationResult holds the outcome for one resource.
type ValidationResult struct {
    Index int
    Valid bool
    Error error
}

func validateBatch(
    resources [][]byte,
    expression *fhirpath.Expression,
    workers int,
) []ValidationResult {
    jobs := make(chan int, len(resources))
    results := make([]ValidationResult, len(resources))

    var wg sync.WaitGroup
    for w := 0; w < workers; w++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for idx := range jobs {
                result, err := expression.EvaluateWithOptions(resources[idx],
                    fhirpath.WithTimeout(2*time.Second),
                    fhirpath.WithMaxCollectionSize(5000),
                )
                if err != nil {
                    results[idx] = ValidationResult{Index: idx, Error: err}
                    continue
                }

                // Check if the expression returned true (valid).
                valid := false
                if !result.Empty() {
                    valid = result.String() == "[true]"
                }
                results[idx] = ValidationResult{Index: idx, Valid: valid}
            }
        }()
    }

    // Enqueue all jobs.
    for i := range resources {
        jobs <- i
    }
    close(jobs)

    wg.Wait()
    return results
}

func main() {
    // Compile the validation expression once.
    expr := fhirpath.MustCompile("Patient.name.exists() and Patient.birthDate.exists()")

    resources := [][]byte{
        []byte(`{"resourceType":"Patient","name":[{"family":"Doe"}],"birthDate":"1990-01-15"}`),
        []byte(`{"resourceType":"Patient","name":[{"family":"Smith"}]}`),
        []byte(`{"resourceType":"Patient","birthDate":"1985-03-22"}`),
        []byte(`{"resourceType":"Patient","name":[{"family":"Lee"}],"birthDate":"2000-07-04"}`),
    }

    // Process with 4 worker goroutines.
    results := validateBatch(resources, expr, 4)

    for _, r := range results {
        if r.Error != nil {
            fmt.Printf("resource %d: error: %v\n", r.Index, r.Error)
        } else {
            fmt.Printf("resource %d: valid=%v\n", r.Index, r.Valid)
        }
    }
    // Output:
    // resource 0: valid=true
    // resource 1: valid=false
    // resource 2: valid=false
    // resource 3: valid=true
}

Resumen

ObjetoSeguro para HilosNotas
*ExpressionSiInmutable después de creación; comparta libremente
*ExpressionCacheSiUsa sync.RWMutex internamente
DefaultCacheSi*ExpressionCache global
EvaluateCached(), GetCached()SiDelegan a DefaultCache
eval.ContextNoCreado por evaluación; nunca compartir entre goroutines
Recurso []byteSolo lecturaSeguro si ninguna goroutine lo muta durante evaluación
ReferenceResolverDependeSu implementación debe ser segura para uso concurrente
TerminologyServiceDependeSu implementación debe ser segura para uso concurrente
ProfileValidatorDependeSu implementación debe ser segura para uso concurrente
Última actualización