Files
visualizador_instanciados/backend_go/sparql.go

238 lines
6.3 KiB
Go

package main
import (
"context"
"encoding/base64"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"visualizador_instanciados/backend_go/queryscope"
)
type AnzoGraphClient struct {
cfg Config
endpoint string
basicAuthHeader string
client *http.Client
tokenManager *keycloakTokenManager
}
func NewAnzoGraphClient(cfg Config) *AnzoGraphClient {
endpoint := cfg.EffectiveSparqlEndpoint()
client := &http.Client{}
basicAuthHeader := ""
if !cfg.UsesExternalSparql() {
user := strings.TrimSpace(cfg.SparqlUser)
pass := strings.TrimSpace(cfg.SparqlPass)
if user != "" && pass != "" {
token := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
basicAuthHeader = "Basic " + token
}
}
agc := &AnzoGraphClient{
cfg: cfg,
endpoint: endpoint,
basicAuthHeader: basicAuthHeader,
client: client,
}
if cfg.UsesExternalSparql() {
agc.tokenManager = newKeycloakTokenManager(cfg, client)
}
return agc
}
func (c *AnzoGraphClient) Startup(ctx context.Context) error {
log.Printf(
"[sparql] startup source_mode=%s endpoint=%s auth_mode=%s load_on_start=%t",
c.cfg.SparqlSourceMode,
c.endpoint,
c.authMode(),
c.cfg.SparqlLoadOnStart,
)
if c.cfg.UsesExternalSparql() {
tokenCtx, cancel := context.WithTimeout(ctx, c.cfg.SparqlReadyTimeout)
defer cancel()
if _, err := c.refreshExternalToken(tokenCtx, "startup"); err != nil {
return fmt.Errorf("keycloak startup token fetch failed: %w", err)
}
}
if err := c.waitReady(ctx); err != nil {
return err
}
c.logNamedGraphDatasetProbe(ctx, "startup_initial")
if c.cfg.SparqlLoadOnStart {
df := strings.TrimSpace(c.cfg.SparqlDataFile)
if df == "" {
return fmt.Errorf("SPARQL_LOAD_ON_START=true but SPARQL_DATA_FILE is not set")
}
giri := strings.TrimSpace(c.cfg.SparqlGraphIRI)
if giri != "" {
if err := c.update(ctx, fmt.Sprintf("LOAD <%s> INTO GRAPH <%s>", df, giri)); err != nil {
return err
}
} else {
if err := c.update(ctx, fmt.Sprintf("LOAD <%s>", df)); err != nil {
return err
}
}
if err := c.waitReady(ctx); err != nil {
return err
}
c.logNamedGraphDatasetProbe(ctx, "startup_post_load")
}
return nil
}
func (c *AnzoGraphClient) Shutdown(ctx context.Context) error {
_ = ctx
return nil
}
func (c *AnzoGraphClient) Query(ctx context.Context, query string) ([]byte, error) {
return c.queryWithTimeout(ctx, query, c.cfg.SparqlTimeout)
}
func (c *AnzoGraphClient) queryWithTimeout(ctx context.Context, query string, timeout time.Duration) ([]byte, error) {
resp, _, err := c.queryRequestWithTimeout(ctx, query, timeout)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
func (c *AnzoGraphClient) update(ctx context.Context, update string) error {
ctx2, cancel := context.WithTimeout(ctx, c.cfg.SparqlTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx2, http.MethodPost, c.endpoint, strings.NewReader(update))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/sparql-update")
req.Header.Set("Accept", "application/json")
authHeader, err := c.authorizationHeader(ctx2, "sparql_update")
if err != nil {
return err
}
if authHeader != "" {
req.Header.Set("Authorization", authHeader)
}
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("sparql update failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
return nil
}
func (c *AnzoGraphClient) waitReady(ctx context.Context) error {
var lastErr error
log.Printf(
"[sparql] readiness_wait_start endpoint=%s retries=%d timeout=%s delay=%s query_scope=named_graphs",
c.endpoint,
c.cfg.SparqlReadyRetries,
c.cfg.SparqlReadyTimeout,
c.cfg.SparqlReadyDelay,
)
for i := 0; i < c.cfg.SparqlReadyRetries; i++ {
select {
case <-ctx.Done():
if lastErr != nil {
return fmt.Errorf("anzograph not ready at %s: %w", c.endpoint, lastErr)
}
return ctx.Err()
default:
}
var ask sparqlBooleanResponse
_, err := c.queryJSONWithTimeout(ctx, namedGraphAnyTripleAskQuery(), c.cfg.SparqlReadyTimeout, &ask)
if err == nil {
log.Printf("[sparql] readiness_wait_ok endpoint=%s attempt=%d/%d", c.endpoint, i+1, c.cfg.SparqlReadyRetries)
return nil
}
lastErr = err
log.Printf("[sparql] readiness_wait_retry endpoint=%s attempt=%d/%d err=%v", c.endpoint, i+1, c.cfg.SparqlReadyRetries, err)
time.Sleep(c.cfg.SparqlReadyDelay)
}
return fmt.Errorf("anzograph not ready at %s: %w", c.endpoint, lastErr)
}
func namedGraphAnyTripleAskQuery() string {
return queryscope.AskAnyTripleQuery()
}
func (c *AnzoGraphClient) authMode() string {
switch {
case c.cfg.UsesExternalSparql():
return "bearer"
case strings.HasPrefix(c.basicAuthHeader, "Basic "):
return "basic"
default:
return "none"
}
}
func (c *AnzoGraphClient) authorizationHeader(ctx context.Context, reason string) (string, error) {
if !c.cfg.UsesExternalSparql() {
return c.basicAuthHeader, nil
}
if c.tokenManager == nil {
return "", fmt.Errorf("external sparql mode is enabled but token manager is not configured")
}
token, err := c.tokenManager.EnsureToken(ctx, reason)
if err != nil {
return "", err
}
return "Bearer " + token, nil
}
func (c *AnzoGraphClient) refreshExternalToken(ctx context.Context, reason string) (string, error) {
if !c.cfg.UsesExternalSparql() {
return "", nil
}
if c.tokenManager == nil {
return "", fmt.Errorf("external sparql mode is enabled but token manager is not configured")
}
return c.tokenManager.Refresh(ctx, reason)
}
func (c *AnzoGraphClient) logNamedGraphDatasetProbe(ctx context.Context, stage string) {
var ask sparqlBooleanResponse
metrics, err := c.queryJSONWithTimeout(ctx, namedGraphAnyTripleAskQuery(), c.cfg.SparqlReadyTimeout, &ask)
if err != nil {
log.Printf("[sparql] dataset_probe_failed stage=%s endpoint=%s err=%v", stage, c.endpoint, err)
return
}
log.Printf(
"[sparql] dataset_probe stage=%s endpoint=%s named_graph_has_triples=%t bytes=%d round_trip_time=%s decode_time=%s",
stage,
c.endpoint,
ask.Boolean,
metrics.ResponseBytes,
metrics.RoundTripTime.Truncate(time.Millisecond),
metrics.BodyDecodeTime.Truncate(time.Millisecond),
)
}