Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94ae8589ce | ||
|
|
8d8f7cb097 | ||
|
|
6e9111dd0c | ||
|
|
8c5386ec34 | ||
|
|
38cff16f45 | ||
|
|
2d6baef7a9 | ||
|
|
89924a417a | ||
|
|
6735bca317 | ||
|
|
3c83e2ba5c | ||
|
|
f2cc059e4b | ||
|
|
91c60b8c0c | ||
|
|
1687dad063 | ||
|
|
37560816aa | ||
|
|
f92406da68 | ||
|
|
0131a96de8 | ||
|
|
0d4e9ee1b4 | ||
|
|
9282fc5bf6 | ||
|
|
4c4a85aec6 | ||
|
|
ebcae4cc9c | ||
|
|
9b262609b7 |
@@ -28,6 +28,89 @@ func OpenCapability(ctx context.Context, config ContinuityConfig) (*CapabilityRe
|
||||
|
||||
func (r *CapabilityRepository) Close() { r.pool.Close() }
|
||||
|
||||
func (r *CapabilityRepository) AdmitCognitive(ctx context.Context, leaseID string, policy capability.AdmissionPolicy, request capability.AdmissionRequest, nowUnixMillis int64) (capability.AdmissionDecision, capability.AdmissionLease, error) {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
if leaseID == "" || nowUnixMillis < 1 {
|
||||
return capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonInputBudget, PolicyID: policy.ID}, capability.AdmissionLease{}, nil
|
||||
}
|
||||
var existingState string
|
||||
var existingPolicy string
|
||||
err = tx.QueryRow(ctx, `SELECT state, policy_id FROM capability.cognitive_admission_leases WHERE lease_id=$1`, leaseID).Scan(&existingState, &existingPolicy)
|
||||
if err == nil {
|
||||
if existingState == "ACTIVE" {
|
||||
return capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: existingPolicy}, capability.AdmissionLease{ID: leaseID, PolicyID: existingPolicy}, tx.Commit(ctx)
|
||||
}
|
||||
return capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonConcurrency, PolicyID: existingPolicy}, capability.AdmissionLease{}, tx.Commit(ctx)
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
windowStart := nowUnixMillis - 60*60*1000
|
||||
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_admission_counters(policy_id,window_start_unix_millis,accepted_count,concurrent_count,updated_at_unix_millis) VALUES($1,$2,0,0,$3) ON CONFLICT(policy_id) DO NOTHING`, policy.ID, windowStart, nowUnixMillis)
|
||||
if err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
var storedWindow int64
|
||||
var accepted, concurrent int
|
||||
if err = tx.QueryRow(ctx, `SELECT window_start_unix_millis,accepted_count,concurrent_count FROM capability.cognitive_admission_counters WHERE policy_id=$1 FOR UPDATE`, policy.ID).Scan(&storedWindow, &accepted, &concurrent); err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
if nowUnixMillis-storedWindow >= 60*60*1000 {
|
||||
storedWindow, accepted = windowStart, 0
|
||||
}
|
||||
var expired int64
|
||||
if err = tx.QueryRow(ctx, `WITH expired AS (UPDATE capability.cognitive_admission_leases SET state='EXPIRED' WHERE policy_id=$1 AND state='ACTIVE' AND expires_at_unix_millis <= $2 RETURNING lease_id) SELECT count(*) FROM expired`, policy.ID, nowUnixMillis).Scan(&expired); err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
concurrent -= int(expired)
|
||||
if concurrent < 0 {
|
||||
concurrent = 0
|
||||
}
|
||||
decision := capability.Admit(request, policy, capability.AdmissionCounters{Concurrent: concurrent, AcceptedLastHour: accepted}, nowUnixMillis)
|
||||
if decision.State != capability.AdmissionAdmitted {
|
||||
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET window_start_unix_millis=$2,accepted_count=$3,concurrent_count=$4,updated_at_unix_millis=$5 WHERE policy_id=$1`, policy.ID, storedWindow, accepted, concurrent, nowUnixMillis); err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
return decision, capability.AdmissionLease{}, tx.Commit(ctx)
|
||||
}
|
||||
expires := nowUnixMillis + 60*1000
|
||||
if request.DeadlineMillis > nowUnixMillis && request.DeadlineMillis < expires {
|
||||
expires = request.DeadlineMillis
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET window_start_unix_millis=$2,accepted_count=$3,concurrent_count=$4,updated_at_unix_millis=$5 WHERE policy_id=$1`, policy.ID, storedWindow, accepted+1, concurrent+1, nowUnixMillis); err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_admission_leases(lease_id,policy_id,state,expires_at_unix_millis,created_at_unix_millis) VALUES($1,$2,'ACTIVE',$3,$4)`, leaseID, policy.ID, expires, nowUnixMillis); err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
return decision, capability.AdmissionLease{ID: leaseID, PolicyID: policy.ID}, tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) ReleaseCognitiveAdmission(ctx context.Context, lease capability.AdmissionLease, nowUnixMillis int64) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
var policyID string
|
||||
err = tx.QueryRow(ctx, `UPDATE capability.cognitive_admission_leases SET state='RELEASED' WHERE lease_id=$1 AND state='ACTIVE' RETURNING policy_id`, lease.ID).Scan(&policyID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(ctx, `UPDATE capability.cognitive_admission_counters SET concurrent_count=GREATEST(0,concurrent_count-1),updated_at_unix_millis=$2 WHERE policy_id=$1`, policyID, nowUnixMillis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveHealth(ctx context.Context, health capability.Health) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.health (capability_id, state, reason, revision, semantic_version, observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (capability_id) DO UPDATE SET state=EXCLUDED.state, reason=EXCLUDED.reason, revision=EXCLUDED.revision, semantic_version=EXCLUDED.semantic_version, observed_at_unix_millis=EXCLUDED.observed_at_unix_millis WHERE capability.health.revision < EXCLUDED.revision`, health.CapabilityID, health.State, health.Reason, health.Revision, health.SemanticVersion, health.ObservedAtUnixMillis)
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
type httpAdmissionClient struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newHTTPAdmissionClient(baseURL string, client *http.Client) *httpAdmissionClient {
|
||||
return &httpAdmissionClient{baseURL: strings.TrimRight(baseURL, "/"), client: client}
|
||||
}
|
||||
|
||||
func (c *httpAdmissionClient) Admit(ctx context.Context, leaseID string, request capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) {
|
||||
payload, err := json.Marshal(struct {
|
||||
LeaseID string `json:"lease_id"`
|
||||
Request capability.AdmissionRequest `json:"request"`
|
||||
}{leaseID, request})
|
||||
if err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/internal/cognitive/admission", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, fmt.Errorf("admission status %d", resp.StatusCode)
|
||||
}
|
||||
var result struct {
|
||||
Decision capability.AdmissionDecision `json:"decision"`
|
||||
Lease capability.AdmissionLease `json:"lease"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return capability.AdmissionDecision{}, capability.AdmissionLease{}, err
|
||||
}
|
||||
return result.Decision, result.Lease, nil
|
||||
}
|
||||
|
||||
func (c *httpAdmissionClient) Release(ctx context.Context, lease capability.AdmissionLease) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/internal/cognitive/admission/"+lease.ID+"/release", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("admission release status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
)
|
||||
|
||||
type admissionTestProvider struct{ calls int }
|
||||
|
||||
func (p *admissionTestProvider) Advise(context.Context, cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
|
||||
p.calls++
|
||||
return cognitive.AdvisoryResponse{FinishReason: cognitive.FinishCompleted}, nil
|
||||
}
|
||||
|
||||
type admissionTestClient struct {
|
||||
decision capability.AdmissionDecision
|
||||
releases int
|
||||
}
|
||||
|
||||
func (c *admissionTestClient) Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) {
|
||||
return c.decision, capability.AdmissionLease{ID: "lease-1", PolicyID: "policy-1"}, nil
|
||||
}
|
||||
func (c *admissionTestClient) Release(context.Context, capability.AdmissionLease) error {
|
||||
c.releases++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestInvokeAdmittedDoesNotCallProviderWhenRejected(t *testing.T) {
|
||||
provider := &admissionTestProvider{}
|
||||
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionRejected, Reason: capability.AdmissionReasonConcurrency, PolicyID: "policy-1"}}
|
||||
gateway := cognitiveGateway{admission: client}
|
||||
response, err := gateway.invokeAdmitted(context.Background(), "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
|
||||
if err != nil || response.FinishReason != cognitive.FinishRateLimited || provider.calls != 0 || client.releases != 0 {
|
||||
t.Fatalf("rejected admission leaked provider call: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvokeAdmittedReleasesDurableLeaseAfterProvider(t *testing.T) {
|
||||
provider := &admissionTestProvider{}
|
||||
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: "policy-1"}}
|
||||
gateway := cognitiveGateway{admission: client}
|
||||
response, err := gateway.invokeAdmitted(context.Background(), "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
|
||||
if err != nil || response.FinishReason != cognitive.FinishCompleted || provider.calls != 1 || client.releases != 1 {
|
||||
t.Fatalf("admitted lease was not released: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,11 @@ type bindingResolver interface {
|
||||
RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error)
|
||||
}
|
||||
|
||||
type admissionClient interface {
|
||||
Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error)
|
||||
Release(context.Context, capability.AdmissionLease) error
|
||||
}
|
||||
|
||||
type providerRegistration struct {
|
||||
ProviderInstanceID capability.ProviderInstanceID
|
||||
Kind capability.ProviderKind
|
||||
@@ -46,6 +51,7 @@ type cognitiveGateway struct {
|
||||
now func() time.Time
|
||||
allowCanary bool
|
||||
canaryMaxOutputBytes int
|
||||
admission admissionClient
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) policyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -95,9 +101,9 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
|
||||
started := g.now()
|
||||
response, invokeErr := provider.Advise(r.Context(), invocation)
|
||||
response, invokeErr := g.invokeAdmitted(r.Context(), string(request.RequestID), provider, invocation, maxOutputBytes)
|
||||
if invokeErr != nil {
|
||||
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
|
||||
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable}
|
||||
}
|
||||
g.observe(r.Context(), selected, response, started)
|
||||
if fallbackEligible(response.FinishReason) {
|
||||
@@ -109,9 +115,9 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
|
||||
fallbackRequest.ProviderBindingID, fallbackRequest.ProviderBindingRevision = fallback.Binding.ID, fallback.Binding.Revision
|
||||
fallbackRequest.ProviderInstanceID, fallbackRequest.ModelDescriptorID, fallbackRequest.ProviderModelID = fallback.Provider.ID, fallback.Model.ID, fallback.Model.ProviderModelID
|
||||
fallbackStarted := g.now()
|
||||
fallbackResponse, fallbackErr := fallbackProvider.Advise(r.Context(), fallbackRequest)
|
||||
fallbackResponse, fallbackErr := g.invokeAdmitted(r.Context(), string(request.RequestID)+":fallback", fallbackProvider, fallbackRequest, maxOutputBytes)
|
||||
if fallbackErr != nil {
|
||||
fallbackResponse = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: fallback.Binding.ID, ProviderBindingRevision: fallback.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
|
||||
fallbackResponse = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: fallback.Binding.ID, ProviderBindingRevision: fallback.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable}
|
||||
}
|
||||
g.observe(r.Context(), fallback, fallbackResponse, fallbackStarted)
|
||||
response = fallbackResponse
|
||||
@@ -124,6 +130,22 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
|
||||
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage})
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, provider advisoryProvider, invocation cognitive.AdvisoryRequest, maxOutputBytes int) (cognitive.AdvisoryResponse, error) {
|
||||
var lease capability.AdmissionLease
|
||||
if g.admission != nil {
|
||||
decision, admittedLease, err := g.admission.Admit(ctx, leaseID, capability.AdmissionRequest{InputBytes: len(invocation.Message), MaxOutputBytes: maxOutputBytes, DeadlineMillis: 0})
|
||||
if err != nil {
|
||||
return cognitive.AdvisoryResponse{}, err
|
||||
}
|
||||
if decision.State != capability.AdmissionAdmitted {
|
||||
return cognitive.AdvisoryResponse{Version: invocation.Version, RequestID: invocation.RequestID, ProviderBindingID: invocation.ProviderBindingID, ProviderBindingRevision: invocation.ProviderBindingRevision, FinishReason: cognitive.FinishRateLimited}, nil
|
||||
}
|
||||
lease = admittedLease
|
||||
defer func() { _ = g.admission.Release(ctx, lease) }()
|
||||
}
|
||||
return provider.Advise(ctx, invocation)
|
||||
}
|
||||
|
||||
func selectionObjectiveForMessage(message string) capability.SelectionObjective {
|
||||
lower := strings.ToLower(message)
|
||||
if strings.Contains(lower, "дешев") || strings.Contains(lower, "бесплат") || strings.Contains(lower, "эконом") || strings.Contains(lower, "cost") || strings.Contains(lower, "cheap") {
|
||||
|
||||
@@ -70,15 +70,15 @@ func main() {
|
||||
kind := capability.ProviderKind(envOr("OPENAI_PROVIDER_KIND", "openai-responses-v1"))
|
||||
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: openAIProvider})
|
||||
}
|
||||
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096}
|
||||
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096, admission: newHTTPAdmissionClient(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 5 * time.Second})}
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
readinessClient := &http.Client{Timeout: 2 * time.Second}
|
||||
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: []health.Probe{
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"),
|
||||
health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"),
|
||||
health.HTTPProbe(readinessClient, lifeboatURL),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
|
||||
health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
|
||||
}})
|
||||
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
|
||||
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
|
||||
|
||||
@@ -141,6 +141,34 @@ func main() {
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"),
|
||||
}})
|
||||
admissionPolicy := capability.AdmissionPolicy{ID: envOr("COGNITIVE_ADMISSION_POLICY_ID", "creator-advisory-local-v1"), MaxConcurrent: envInt("COGNITIVE_ADMISSION_MAX_CONCURRENT", 1), MaxAcceptedPerHour: envInt("COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR", 60), MaxInputBytes: envInt("COGNITIVE_ADMISSION_MAX_INPUT_BYTES", 80<<10), MaxOutputBytes: envInt("COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES", 16<<10)}
|
||||
mux.HandleFunc("POST /v1/internal/cognitive/admission", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
LeaseID string `json:"lease_id"`
|
||||
Request capability.AdmissionRequest `json:"request"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&request); err != nil || request.LeaseID == "" {
|
||||
http.Error(w, "invalid admission request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
decision, lease, err := capabilityRepository.AdmitCognitive(r.Context(), request.LeaseID, admissionPolicy, request.Request, time.Now().UnixMilli())
|
||||
if err != nil {
|
||||
http.Error(w, "admission unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(struct {
|
||||
Decision capability.AdmissionDecision `json:"decision"`
|
||||
Lease capability.AdmissionLease `json:"lease"`
|
||||
}{decision, lease})
|
||||
})
|
||||
mux.HandleFunc("POST /v1/internal/cognitive/admission/{lease}/release", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := capabilityRepository.ReleaseCognitiveAdmission(r.Context(), capability.AdmissionLease{ID: r.PathValue("lease")}, time.Now().UnixMilli()); err != nil {
|
||||
http.Error(w, "admission release unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
|
||||
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
@@ -1159,6 +1187,14 @@ func envOr(name, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envInt(name string, fallback int) int {
|
||||
value, err := strconv.Atoi(os.Getenv(name))
|
||||
if err != nil || value < 1 {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string {
|
||||
if inventoryErr != nil {
|
||||
return "Я не смогла прочитать реестр подключённых моделей. Встроенный канал Домового остаётся доступен; настройки можно проверить повторно после восстановления реестра. Ключи и пароли не выводятся в чат."
|
||||
|
||||
@@ -27,10 +27,10 @@ type AdmissionPolicy struct {
|
||||
}
|
||||
|
||||
type AdmissionRequest struct {
|
||||
Complexity AdmissionComplexity
|
||||
InputBytes int
|
||||
MaxOutputBytes int
|
||||
DeadlineMillis int64
|
||||
Complexity AdmissionComplexity `json:"complexity"`
|
||||
InputBytes int `json:"input_bytes"`
|
||||
MaxOutputBytes int `json:"max_output_bytes"`
|
||||
DeadlineMillis int64 `json:"deadline_millis"`
|
||||
}
|
||||
|
||||
type AdmissionCounters struct {
|
||||
@@ -39,9 +39,14 @@ type AdmissionCounters struct {
|
||||
}
|
||||
|
||||
type AdmissionDecision struct {
|
||||
State AdmissionDecisionState
|
||||
Reason AdmissionReason
|
||||
PolicyID string
|
||||
State AdmissionDecisionState `json:"state"`
|
||||
Reason AdmissionReason `json:"reason"`
|
||||
PolicyID string `json:"policy_id"`
|
||||
}
|
||||
|
||||
type AdmissionLease struct {
|
||||
ID string `json:"id"`
|
||||
PolicyID string `json:"policy_id"`
|
||||
}
|
||||
|
||||
func Admit(request AdmissionRequest, policy AdmissionPolicy, counters AdmissionCounters, nowMillis int64) AdmissionDecision {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE capability_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.cognitive_admission_counters (
|
||||
policy_id TEXT PRIMARY KEY,
|
||||
window_start_unix_millis BIGINT NOT NULL,
|
||||
accepted_count INTEGER NOT NULL DEFAULT 0,
|
||||
concurrent_count INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (policy_id <> ''), CHECK (window_start_unix_millis > 0),
|
||||
CHECK (accepted_count >= 0), CHECK (concurrent_count >= 0),
|
||||
CHECK (updated_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.cognitive_admission_leases (
|
||||
lease_id TEXT PRIMARY KEY,
|
||||
policy_id TEXT NOT NULL REFERENCES capability.cognitive_admission_counters(policy_id),
|
||||
state TEXT NOT NULL,
|
||||
expires_at_unix_millis BIGINT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (lease_id <> ''), CHECK (state IN ('ACTIVE','RELEASED','EXPIRED')),
|
||||
CHECK (expires_at_unix_millis > 0), CHECK (created_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS cognitive_admission_lease_expiry_idx
|
||||
ON capability.cognitive_admission_leases(policy_id, state, expires_at_unix_millis);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
|
||||
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
|
||||
POSTGRES_CONTAINER="${POSTGRES_CONTAINER:-assistant-postgres-1}"
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "migration runner: env file is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# The deployment env file is controlled by the production operator. Values
|
||||
# are used only by the local Docker/psql boundary and are never logged.
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
: "${POSTGRES_SUPERUSER:?POSTGRES_SUPERUSER is required}"
|
||||
POSTGRES_SUPERUSER="${POSTGRES_SUPERUSER//$'\r'/}"
|
||||
DATABASE="${POSTGRES_DB:-assistant}"
|
||||
DATABASE="${DATABASE//$'\r'/}"
|
||||
|
||||
psql_exec() {
|
||||
docker exec -i "$POSTGRES_CONTAINER" psql -v ON_ERROR_STOP=1 -U "$POSTGRES_SUPERUSER" -d "$DATABASE" "$@"
|
||||
}
|
||||
|
||||
psql_exec -c "CREATE TABLE IF NOT EXISTS public.assistant_migration_history (migration_id TEXT PRIMARY KEY, applied_at_unix_millis BIGINT NOT NULL CHECK (applied_at_unix_millis > 0));"
|
||||
|
||||
if [[ -n "${MIGRATION_BASELINE:-}" ]]; then
|
||||
if [[ ! "$MIGRATION_BASELINE" =~ ^[0-9]{3}$ ]]; then
|
||||
echo "migration runner: invalid MIGRATION_BASELINE" >&2
|
||||
exit 1
|
||||
fi
|
||||
now_ms="$(date +%s%3N)"
|
||||
for file in "$REPO_DIR"/database/migrations/[0-9][0-9][0-9]_*.sql; do
|
||||
migration_id="$(basename "$file" .sql)"
|
||||
number="${migration_id:0:3}"
|
||||
if (( 10#$number <= 10#$MIGRATION_BASELINE )); then
|
||||
psql_exec -c "INSERT INTO public.assistant_migration_history(migration_id, applied_at_unix_millis) VALUES ('$migration_id', $now_ms) ON CONFLICT (migration_id) DO NOTHING;"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
for file in "$REPO_DIR"/database/migrations/[0-9][0-9][0-9]_*.sql; do
|
||||
[[ -f "$file" ]] || continue
|
||||
migration_id="$(basename "$file" .sql)"
|
||||
applied="$(psql_exec -At -c "SELECT 1 FROM public.assistant_migration_history WHERE migration_id='$migration_id' LIMIT 1;")"
|
||||
[[ "$applied" == "1" ]] && continue
|
||||
psql_exec < "$file"
|
||||
now_ms="$(date +%s%3N)"
|
||||
psql_exec -c "INSERT INTO public.assistant_migration_history(migration_id, applied_at_unix_millis) VALUES ('$migration_id', $now_ms);"
|
||||
done
|
||||
@@ -80,6 +80,11 @@ services:
|
||||
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
|
||||
INTERACTION_POSTGRES_PASSWORD_FILE: /run/secrets/interaction_db_password
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
COGNITIVE_ADMISSION_POLICY_ID: creator-advisory-local-v1
|
||||
COGNITIVE_ADMISSION_MAX_CONCURRENT: ${COGNITIVE_ADMISSION_MAX_CONCURRENT:-1}
|
||||
COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR: ${COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR:-60}
|
||||
COGNITIVE_ADMISSION_MAX_INPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_INPUT_BYTES:-81920}
|
||||
COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES:-16384}
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000140-research-evaluation-runtime.json",
|
||||
"task_id": "DEV-000140",
|
||||
"task_revision": 1
|
||||
"task_file": "dev/tasks/DEV-000141-optional-fleet-readiness.json",
|
||||
"task_id": "DEV-000141",
|
||||
"task_revision": 2
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000115",
|
||||
"task_revision": 1,
|
||||
"task_revision": 6,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "e83cf35",
|
||||
"base_repo_revision": "9b26260",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add a deterministic cognitive admission policy for concurrency, accepted/hour, deadline and input/output budgets without changing provider routing or bindings.",
|
||||
"requirement_ids": ["WP-203", "Bounded concurrency/rate limits", "Explainable admission decision"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-admission"], "paths": ["contracts/capability/**", "modules/capability/domain/**", "tests/cognitive_admission/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000115-cognitive-admission-control.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "migrations/**", "deploy/**", "security/**", "cmd/**", "runtime/**", "adapters/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["Existing cognitive capability registry and LimitPolicyRef", "ADR-0006 bounded advisory boundary"], "may_change": ["Typed admission request/policy/decision contract"]},
|
||||
"intent": "Run bounded cognitive admission as a durable capability-owner lease with restart recovery, while preserving provider routing and explicit fallback.",
|
||||
"requirement_ids": ["WP-203", "Bounded concurrency/rate limits", "Durable admission lease", "Explainable admission decision"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-admission", "core-runtime", "cognitive-worker", "deployment"], "paths": ["contracts/capability/**", "adapters/postgres/capability_repository.go", "database/migrations/033_cognitive_admission_leases.sql", "cmd/core-runtime/main.go", "cmd/cognitive-worker/**", "deploy/docker-compose.prod.yml", "deploy/apply-migrations.sh", "tests/cognitive_admission/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000115-cognitive-admission-control.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "modules/authority/**", "modules/work/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
|
||||
"contracts": {"consume": ["Existing cognitive capability registry and LimitPolicyRef", "ADR-0006 bounded advisory boundary", "capability-owned PostgreSQL schema"], "may_change": ["Typed admission request/policy/decision and lease contract"]},
|
||||
"semantic_owner": "capability-cognitive-admission",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["request + policy + observed counters -> admitted or typed rejection"],
|
||||
"side_effects_and_privileges": ["Pure policy evaluation; no provider call, queue, persistence, binding mutation or external mutation"],
|
||||
"persistence_and_migrations": ["No schema or migration changes"],
|
||||
"failure_retry_restart": ["Rejected requests carry a reason; retry requires a fresh policy evaluation"],
|
||||
"assumptions": ["Counters are supplied by a later durable admission implementation", "Policy does not choose or activate providers", "Deadline and budgets are request requirements"],
|
||||
"acceptance_scenarios": ["Concurrency limit rejects excess request", "Accepted/hour limit rejects excess request", "Deadline and byte budget reject invalid request", "Admitted decision exposes reason and policy reference", "Simple/normal/heavy complexity remains a routing input, not authority"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check", "admission policy tests"],
|
||||
"canonical_state": ["capability.cognitive_admission_counters", "capability.cognitive_admission_leases"],
|
||||
"state_transitions": ["request + policy + durable counters -> admitted lease or typed rejection", "active lease -> released/expired"],
|
||||
"side_effects_and_privileges": ["Capability database role only; internal core endpoint; provider calls remain behind cognitive gateway"],
|
||||
"persistence_and_migrations": ["database/migrations/033_cognitive_admission_leases.sql", "deploy/apply-migrations.sh records applied migration IDs and is idempotent"],
|
||||
"failure_retry_restart": ["Expired leases are recovered on next admission transaction; duplicate request IDs are idempotent; rejected requests fall back locally"],
|
||||
"assumptions": ["Core Runtime remains the capability-owner admission boundary", "Policy does not choose or activate providers", "Deadline and budgets are request requirements"],
|
||||
"acceptance_scenarios": ["Concurrency limit rejects excess request", "Accepted/hour limit rejects excess request", "Deadline and byte budget reject invalid request", "Admitted decision exposes reason and policy reference", "expired lease does not permanently block after restart", "cognitive worker uses admission before provider and releases lease"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check", "compose config", "admission policy and HTTP boundary tests"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000141",
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "38cff16",
|
||||
"risk_tier": "E",
|
||||
"intent": "Keep cognitive readiness available when an optional opportunistic fleet member is unavailable while preserving typed degraded evidence.",
|
||||
"requirement_ids": ["WP-003", "WP-203", "Fleet failover readiness"],
|
||||
"allowed_change_scope": {"modules": ["runtime-health", "cognitive-worker"], "paths": ["runtime/health/**", "cmd/cognitive-worker/main.go", "runtime/health/server_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000141-optional-fleet-readiness.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "deploy/**", "security/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["Existing readiness Probe and typed readiness response"], "may_change": ["Optional dependency probe behavior"]},
|
||||
"semantic_owner": "runtime-health and cognitive-worker",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["optional provider unavailable -> readiness remains ready with typed degraded check", "required dependency unavailable -> readiness remains degraded"],
|
||||
"side_effects_and_privileges": ["No external mutation; no provider invocation"],
|
||||
"persistence_and_migrations": ["None"],
|
||||
"failure_retry_restart": ["Optional provider can recover on subsequent readiness probe"],
|
||||
"assumptions": ["CPU/Lifeboat remains a qualified fallback when opportunistic GPU is unavailable"],
|
||||
"acceptance_scenarios": ["GPU timeout does not hide degraded evidence or make fallback unavailable", "required dependency failure still returns 503"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -26,8 +26,9 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type Probe struct {
|
||||
Name string
|
||||
Check func(context.Context) error
|
||||
Name string
|
||||
Check func(context.Context) error
|
||||
Optional bool
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -59,11 +60,13 @@ func (h ReadinessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
err := probe.Check(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
ready = false
|
||||
result.State, result.Reason, result.Detail = "DEGRADED", "DEPENDENCY_UNAVAILABLE", err.Error()
|
||||
if !probe.Optional {
|
||||
ready = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.State != "READY" {
|
||||
if result.State != "READY" && !probe.Optional {
|
||||
ready = false
|
||||
}
|
||||
results = append(results, result)
|
||||
@@ -96,6 +99,15 @@ func HTTPProbe(client *http.Client, endpoint string) Probe {
|
||||
}}
|
||||
}
|
||||
|
||||
// OptionalHTTPProbe reports dependency degradation without making the whole
|
||||
// runtime unavailable. This is used for opportunistic fleet members when a
|
||||
// qualified local fallback remains available.
|
||||
func OptionalHTTPProbe(client *http.Client, endpoint string) Probe {
|
||||
probe := HTTPProbe(client, endpoint)
|
||||
probe.Optional = true
|
||||
return probe
|
||||
}
|
||||
|
||||
func TCPProbe(name, host, port string) Probe {
|
||||
return Probe{Name: name, Check: func(ctx context.Context) error {
|
||||
connection, err := (&net.Dialer{}).DialContext(ctx, "tcp", net.JoinHostPort(host, port))
|
||||
|
||||
@@ -26,3 +26,16 @@ func TestReadinessReturns200WhenAllProbesPass(t *testing.T) {
|
||||
t.Fatalf("readiness response = %d %s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadinessStaysAvailableWithOptionalDependencyDegraded(t *testing.T) {
|
||||
handler := ReadinessHandler{Runtime: "test", Probes: []Probe{
|
||||
{Name: "lifeboat", Check: func(context.Context) error { return nil }},
|
||||
{Name: "opportunistic-gpu", Optional: true, Check: func(context.Context) error { return errors.New("timeout") }},
|
||||
}}
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/readyz", nil))
|
||||
body := recorder.Body.String()
|
||||
if recorder.Code != http.StatusOK || !strings.Contains(body, `"status":"ready"`) || !strings.Contains(body, `"state":"DEGRADED"`) {
|
||||
t.Fatalf("readiness response = %d %s", recorder.Code, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cognitive_admission_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -26,6 +28,13 @@ func TestAdmitReturnsExplainableDecision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdmissionRequestUsesStableWireFields(t *testing.T) {
|
||||
raw, err := json.Marshal(capability.AdmissionRequest{Complexity: capability.AdmissionNormal, InputBytes: 10, MaxOutputBytes: 20, DeadlineMillis: 30})
|
||||
if err != nil || !strings.Contains(string(raw), `"input_bytes":10`) || !strings.Contains(string(raw), `"max_output_bytes":20`) || !strings.Contains(string(raw), `"deadline_millis":30`) {
|
||||
t.Fatalf("admission wire shape drifted: %s %v", raw, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdmitRejectsBudgetAndDeadline(t *testing.T) {
|
||||
policy := capability.AdmissionPolicy{ID: "local-v1", MaxConcurrent: 1, MaxAcceptedPerHour: 1, MaxInputBytes: 10, MaxOutputBytes: 10}
|
||||
if decision := capability.Admit(capability.AdmissionRequest{InputBytes: 11, MaxOutputBytes: 1}, policy, capability.AdmissionCounters{}, 1); decision.Reason != capability.AdmissionReasonInputBudget {
|
||||
|
||||
Reference in New Issue
Block a user