172 lines
7.3 KiB
Go
172 lines
7.3 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"browser.local/platform/domain"
|
|
"browser.local/platform/repo"
|
|
"browser.local/platform/validator"
|
|
)
|
|
|
|
const scumSchemaProbeExecutionKind = "sqlite.schema.probe"
|
|
|
|
func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstanceID, idempotencyKey string) (domain.SCUMSchemaProbeRequest, domain.RemoteAdapterResult, error) {
|
|
idempotencyKey = strings.TrimSpace(idempotencyKey)
|
|
if idempotencyKey == "" {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("idempotencyKey is required")
|
|
}
|
|
instance, err := svc.GetServerInstanceForSession(sessionID, serverInstanceID)
|
|
if err != nil {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
|
}
|
|
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
|
if err != nil {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
|
}
|
|
probe := plugin.SCUMLiveData.Probe
|
|
if plugin.SCUMLiveData.SchemaVersion == "" || probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || strings.TrimSpace(probe.TargetKey) == "" {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe is not declared by the plugin")
|
|
}
|
|
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
|
if err != nil {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
|
}
|
|
if endpoint.Status != domain.RunEndpointStatusOnline && endpoint.Status != domain.RunEndpointStatusDegraded {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run is not online for SCUM schema probe")
|
|
}
|
|
if !containsString(endpoint.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("bound Run does not expose the generic SQLite schema-probe executor")
|
|
}
|
|
binding, err := svc.runtimeBindingForServer(instance.ID)
|
|
if errors.Is(err, repo.ErrNotFound) {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("runtime binding is required before SCUM schema probe")
|
|
}
|
|
if err != nil {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
|
}
|
|
adapterVersion := scumSchemaProbeAdapterVersion(plugin.SCUMLiveData)
|
|
if adapterVersion == "" {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema-probe adapter version is not declared")
|
|
}
|
|
bounds := probe.Bounds
|
|
if bounds.MaxObjects == 0 {
|
|
bounds = domain.DefaultSCUMSchemaProbeBounds()
|
|
}
|
|
jobID := jobIDFromParts("job-remote-adapter", instance.ID, idempotencyKey)
|
|
request := domain.SCUMSchemaProbeRequest{
|
|
RequestID: jobID,
|
|
JobID: jobID,
|
|
Binding: domain.SCUMBindingIdentity{
|
|
ServerInstanceID: instance.ID,
|
|
RunBindingID: binding.ID,
|
|
RunEndpointID: instance.RunEndpointID,
|
|
PluginID: plugin.ID,
|
|
PluginVersion: plugin.Version,
|
|
AdapterVersion: adapterVersion,
|
|
DatabaseIdentity: "logical:" + probe.TargetKey,
|
|
},
|
|
Bounds: bounds,
|
|
RequestedAt: svc.now(),
|
|
}
|
|
if err := validator.ValidateSCUMSchemaProbeRequest(request); err != nil {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
|
}
|
|
result, err := svc.RequestRemoteAdapterForSession(sessionID, domain.RemoteAdapterRequest{
|
|
ServerInstanceID: instance.ID,
|
|
DeclarationKey: probe.TargetKey,
|
|
TargetKey: probe.TargetKey,
|
|
Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe,
|
|
TimeoutSeconds: scumSchemaProbeTimeoutSeconds(bounds),
|
|
MaxAttempts: 1,
|
|
IdempotencyKey: idempotencyKey,
|
|
InputRef: "input://scum-schema-probe/" + instance.ID + "/" + idempotencyKey,
|
|
Inputs: scumSchemaProbeInputs(request, probe.TargetKey),
|
|
PlatformScheduled: true,
|
|
})
|
|
if err != nil {
|
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
|
}
|
|
return request, result, nil
|
|
}
|
|
|
|
func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string {
|
|
for _, gate := range manifest.CapabilityGates {
|
|
if gate.Capability == domain.SCUMDataCapabilitySchemaProbe {
|
|
return strings.TrimSpace(gate.AdapterVersion)
|
|
}
|
|
}
|
|
for _, gate := range manifest.CapabilityGates {
|
|
if strings.TrimSpace(gate.AdapterVersion) != "" {
|
|
return strings.TrimSpace(gate.AdapterVersion)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func scumSchemaProbeTimeoutSeconds(bounds domain.SCUMSchemaProbeBounds) int {
|
|
if bounds.TimeoutMS <= 0 {
|
|
return 1
|
|
}
|
|
seconds := (bounds.TimeoutMS + 999) / 1000
|
|
if seconds <= 0 {
|
|
return 1
|
|
}
|
|
return seconds
|
|
}
|
|
|
|
func scumSchemaProbeInputs(request domain.SCUMSchemaProbeRequest, targetKey string) map[string]string {
|
|
return map[string]string{
|
|
"requestId": request.RequestID,
|
|
"jobId": request.JobID,
|
|
"targetKey": targetKey,
|
|
"serverInstanceId": request.Binding.ServerInstanceID,
|
|
"runBindingId": request.Binding.RunBindingID,
|
|
"runEndpointId": request.Binding.RunEndpointID,
|
|
"pluginId": request.Binding.PluginID,
|
|
"pluginVersion": request.Binding.PluginVersion,
|
|
"adapterVersion": request.Binding.AdapterVersion,
|
|
"gameVersion": request.Binding.GameVersion,
|
|
"databaseIdentity": request.Binding.DatabaseIdentity,
|
|
"maxObjects": strconv.Itoa(request.Bounds.MaxObjects),
|
|
"maxColumnsPerObject": strconv.Itoa(request.Bounds.MaxColumnsPerObject),
|
|
"maxIndexesPerObject": strconv.Itoa(request.Bounds.MaxIndexesPerObject),
|
|
"maxForeignKeys": strconv.Itoa(request.Bounds.MaxForeignKeys),
|
|
"maxCardinalityReads": strconv.Itoa(request.Bounds.MaxCardinalityReads),
|
|
"maxSampleRows": strconv.Itoa(request.Bounds.MaxSampleRows),
|
|
"timeoutMs": strconv.Itoa(request.Bounds.TimeoutMS),
|
|
"maxResultBytes": strconv.Itoa(request.Bounds.MaxResultBytes),
|
|
}
|
|
}
|
|
|
|
func scumSchemaProbeBindingFromInputs(inputs map[string]string) domain.SCUMBindingIdentity {
|
|
return domain.SCUMBindingIdentity{
|
|
ServerInstanceID: inputs["serverInstanceId"],
|
|
RunBindingID: inputs["runBindingId"],
|
|
RunEndpointID: inputs["runEndpointId"],
|
|
PluginID: inputs["pluginId"],
|
|
PluginVersion: inputs["pluginVersion"],
|
|
AdapterVersion: inputs["adapterVersion"],
|
|
GameVersion: inputs["gameVersion"],
|
|
DatabaseIdentity: inputs["databaseIdentity"],
|
|
}
|
|
}
|
|
|
|
func validateSCUMSchemaProbeResultForJob(job domain.Job, result domain.SCUMSchemaProbeResult) error {
|
|
if err := validator.ValidateSCUMSchemaProbeResult(result); err != nil {
|
|
return err
|
|
}
|
|
if result.JobID != job.ID || result.JobID != job.ExecutionInput.Inputs["jobId"] || result.RequestID != job.ExecutionInput.Inputs["requestId"] {
|
|
return validationError("SQLite schema probe result does not match leased job identity")
|
|
}
|
|
if !sameSCUMSchemaProbeBinding(result.Binding, scumSchemaProbeBindingFromInputs(job.ExecutionInput.Inputs)) {
|
|
return validationError("SQLite schema probe result does not match leased binding identity")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sameSCUMSchemaProbeBinding(a, b domain.SCUMBindingIdentity) bool {
|
|
return a.ServerInstanceID == b.ServerInstanceID && a.RunBindingID == b.RunBindingID && a.RunEndpointID == b.RunEndpointID && a.PluginID == b.PluginID && a.PluginVersion == b.PluginVersion && a.AdapterVersion == b.AdapterVersion && a.GameVersion == b.GameVersion && a.DatabaseIdentity == b.DatabaseIdentity
|
|
}
|