backend: support external SPARQL and named-graph snapshots
This commit is contained in:
@@ -5,50 +5,68 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"visualizador_instanciados/backend_go/queryscope"
|
||||
)
|
||||
|
||||
type AnzoGraphClient struct {
|
||||
cfg Config
|
||||
endpoint string
|
||||
authHeader string
|
||||
client *http.Client
|
||||
cfg Config
|
||||
endpoint string
|
||||
basicAuthHeader string
|
||||
client *http.Client
|
||||
tokenManager *keycloakTokenManager
|
||||
}
|
||||
|
||||
func NewAnzoGraphClient(cfg Config) *AnzoGraphClient {
|
||||
endpoint := cfg.EffectiveSparqlEndpoint()
|
||||
authHeader := ""
|
||||
user := strings.TrimSpace(cfg.SparqlUser)
|
||||
pass := strings.TrimSpace(cfg.SparqlPass)
|
||||
if user != "" && pass != "" {
|
||||
token := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
|
||||
authHeader = "Basic " + token
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return &AnzoGraphClient{
|
||||
cfg: cfg,
|
||||
endpoint: endpoint,
|
||||
authHeader: authHeader,
|
||||
client: &http.Client{},
|
||||
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
|
||||
}
|
||||
|
||||
if c.cfg.SparqlClearOnStart {
|
||||
if err := c.update(ctx, "CLEAR ALL"); err != nil {
|
||||
return 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)
|
||||
@@ -68,6 +86,7 @@ func (c *AnzoGraphClient) Startup(ctx context.Context) error {
|
||||
if err := c.waitReady(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
c.logNamedGraphDatasetProbe(ctx, "startup_post_load")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -83,23 +102,7 @@ func (c *AnzoGraphClient) Query(ctx context.Context, query string) ([]byte, erro
|
||||
}
|
||||
|
||||
func (c *AnzoGraphClient) queryWithTimeout(ctx context.Context, query string, timeout time.Duration) ([]byte, error) {
|
||||
ctx2, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("query", query)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx2, http.MethodPost, c.endpoint, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/sparql-results+json")
|
||||
if c.authHeader != "" {
|
||||
req.Header.Set("Authorization", c.authHeader)
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
resp, _, err := c.queryRequestWithTimeout(ctx, query, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -109,9 +112,6 @@ func (c *AnzoGraphClient) queryWithTimeout(ctx context.Context, query string, ti
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("sparql query failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
@@ -125,8 +125,12 @@ func (c *AnzoGraphClient) update(ctx context.Context, update string) error {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/sparql-update")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if c.authHeader != "" {
|
||||
req.Header.Set("Authorization", c.authHeader)
|
||||
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)
|
||||
@@ -144,6 +148,13 @@ func (c *AnzoGraphClient) update(ctx context.Context, update string) error {
|
||||
|
||||
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():
|
||||
@@ -154,16 +165,73 @@ func (c *AnzoGraphClient) waitReady(ctx context.Context) error {
|
||||
default:
|
||||
}
|
||||
|
||||
body, err := c.queryWithTimeout(ctx, "ASK WHERE { ?s ?p ?o }", c.cfg.SparqlReadyTimeout)
|
||||
var ask sparqlBooleanResponse
|
||||
_, err := c.queryJSONWithTimeout(ctx, namedGraphAnyTripleAskQuery(), c.cfg.SparqlReadyTimeout, &ask)
|
||||
if err == nil {
|
||||
// Ensure it's JSON, not HTML/text during boot.
|
||||
if strings.HasPrefix(strings.TrimSpace(string(body)), "{") {
|
||||
return nil
|
||||
}
|
||||
err = fmt.Errorf("unexpected readiness response: %s", strings.TrimSpace(string(body)))
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user