Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78f2ecc246 | ||
|
|
8c97445f7e | ||
|
|
0ea5b3b738 | ||
|
|
66b785f7cb | ||
|
|
cb14194a5f | ||
|
|
9c38e1bc57 | ||
|
|
4ac99e43ae |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"pid": 44608,
|
"pid": 48400,
|
||||||
"version": "0.9.9",
|
"version": "0.9.9",
|
||||||
"socketPath": "\\\\.\\pipe\\codegraph-97b92efdcc5351da",
|
"socketPath": "\\\\.\\pipe\\codegraph-97b92efdcc5351da",
|
||||||
"startedAt": 1781240018712
|
"startedAt": 1783060842523
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ UI-правила репозитория: `.cursor/rules/web-shadcn.mdc` (MCP +
|
|||||||
1. **Контракт HTTP** — `docs/openapi.yaml` (не Context7).
|
1. **Контракт HTTP** — `docs/openapi.yaml` (не Context7).
|
||||||
2. **Context7** — синтаксис и API библиотек из таблицы.
|
2. **Context7** — синтаксис и API библиотек из таблицы.
|
||||||
3. **Локальные docs** — `docs/`, `web/README.md`, `AGENTS.md`.
|
3. **Локальные docs** — `docs/`, `web/README.md`, `AGENTS.md`.
|
||||||
4. **Официальный сайт** — BIRD: https://bird.network.cz/?get_doc (если Context7 не покрыл кейс).
|
4. **Официальный сайт** — BIRD: https://bird.nic.cz/?get_doc (если Context7 не покрыл кейс).
|
||||||
|
|
||||||
## Примеры запросов
|
## Примеры запросов
|
||||||
|
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ alwaysApply: true
|
|||||||
| OpenAPI / problem+json | `docs/openapi.yaml`, RFC 9457 |
|
| OpenAPI / problem+json | `docs/openapi.yaml`, RFC 9457 |
|
||||||
| Svelte / Kit | https://svelte.dev/docs , https://kit.svelte.dev/docs |
|
| Svelte / Kit | https://svelte.dev/docs , https://kit.svelte.dev/docs |
|
||||||
| shadcn-svelte | https://shadcn-svelte.com/docs |
|
| shadcn-svelte | https://shadcn-svelte.com/docs |
|
||||||
| BIRD 2 | https://bird.network.cz/?get_doc |
|
| BIRD 2 | https://bird.nic.cz/?get_doc |
|
||||||
| Prometheus Go | https://pkg.go.dev/github.com/prometheus/client_golang |
|
| Prometheus Go | https://pkg.go.dev/github.com/prometheus/client_golang |
|
||||||
|
|
||||||
**DOC-SYNC-01** | MUST | Новый API библиотеки — сверка версии в `go.mod`/`package.json` с официальной документацией.
|
**DOC-SYNC-01** | MUST | Новый API библиотеки — сверка версии в `go.mod`/`package.json` с официальной документацией.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ alwaysApply: false
|
|||||||
| Параметры BIRD tenant | Global settings: `bird_router_id`, `bird_local_asn`, … (`docs/manual.md`) |
|
| Параметры BIRD tenant | Global settings: `bird_router_id`, `bird_local_asn`, … (`docs/manual.md`) |
|
||||||
| BGP peers | `BGPPeer` + `ParsePeerNeighbor` |
|
| BGP peers | `BGPPeer` + `ParsePeerNeighbor` |
|
||||||
|
|
||||||
**BIRD2 docs:** https://bird.network.cz/?get_doc
|
**BIRD2 docs:** https://bird.nic.cz/?get_doc
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ alwaysApply: false
|
|||||||
|
|
||||||
## Documentation Sync
|
## Documentation Sync
|
||||||
|
|
||||||
**DOC-SYNC-05** | MUST | BIRD — https://bird.network.cz/?get_doc
|
**DOC-SYNC-05** | MUST | BIRD — https://bird.nic.cz/?get_doc
|
||||||
**DOC-SYNC-08** | MUST | BGP policy — RFC 4271, 4760, 7454 + BIRD docs + `birdfmt`
|
**DOC-SYNC-08** | MUST | BGP policy — RFC 4271, 4760, 7454 + BIRD docs + `birdfmt`
|
||||||
**DOC-SYNC-09** | MUST | CIDR — https://pkg.go.dev/net/netip ; примеры — RFC 5737, 3849
|
**DOC-SYNC-09** | MUST | CIDR — https://pkg.go.dev/net/netip ; примеры — RFC 5737, 3849
|
||||||
|
|
||||||
|
|||||||
@@ -167,14 +167,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
cache: pnpm
|
- name: Enable pnpm via corepack
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
run: corepack enable
|
||||||
- name: pnpm install, typecheck, lint, build
|
- name: pnpm install, typecheck, lint, build
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
} from '@evobgp/ui/components/select'
|
} from '@evobgp/ui/components/select'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
|
||||||
import {
|
import {
|
||||||
@@ -378,7 +379,9 @@ function DiffTab({ revisions }: { revisions: import('@/types/api').RevisionRow[]
|
|||||||
<div className="flex w-full max-w-xs flex-col gap-1">
|
<div className="flex w-full max-w-xs flex-col gap-1">
|
||||||
<span className="text-xs text-muted-foreground">Ревизия A</span>
|
<span className="text-xs text-muted-foreground">Ревизия A</span>
|
||||||
<Select value={a} onValueChange={(v) => v && setA(v)}>
|
<Select value={a} onValueChange={(v) => v && setA(v)}>
|
||||||
<SelectTrigger>{a ? a.slice(0, 12) + '…' : 'Выберите'}</SelectTrigger>
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Выберите" />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{revisions.map((r) => (
|
{revisions.map((r) => (
|
||||||
<SelectItem key={r.id} value={r.id}>
|
<SelectItem key={r.id} value={r.id}>
|
||||||
@@ -391,7 +394,9 @@ function DiffTab({ revisions }: { revisions: import('@/types/api').RevisionRow[]
|
|||||||
<div className="flex w-full max-w-xs flex-col gap-1">
|
<div className="flex w-full max-w-xs flex-col gap-1">
|
||||||
<span className="text-xs text-muted-foreground">Ревизия B</span>
|
<span className="text-xs text-muted-foreground">Ревизия B</span>
|
||||||
<Select value={b} onValueChange={(v) => v && setB(v)}>
|
<Select value={b} onValueChange={(v) => v && setB(v)}>
|
||||||
<SelectTrigger>{b ? b.slice(0, 12) + '…' : 'Выберите'}</SelectTrigger>
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Выберите" />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{revisions.map((r) => (
|
{revisions.map((r) => (
|
||||||
<SelectItem key={r.id} value={r.id}>
|
<SelectItem key={r.id} value={r.id}>
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import { useQuery } from '@tanstack/react-query'
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
|
||||||
import { Input } from '@evobgp/ui/components/input'
|
import { Input } from '@evobgp/ui/components/input'
|
||||||
import { Label } from '@evobgp/ui/components/label'
|
import { Label } from '@evobgp/ui/components/label'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@evobgp/ui/components/select'
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@evobgp/ui/components/select'
|
||||||
|
|
||||||
import { PageHeader } from '@/components/page-header'
|
import { PageHeader } from '@/components/page-header'
|
||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
@@ -85,7 +91,9 @@ function SettingsComponent() {
|
|||||||
<CardContent className="flex flex-col gap-2">
|
<CardContent className="flex flex-col gap-2">
|
||||||
<Label htmlFor="theme-select">Тема</Label>
|
<Label htmlFor="theme-select">Тема</Label>
|
||||||
<Select value={theme ?? 'system'} onValueChange={(v) => v && setTheme(v)}>
|
<Select value={theme ?? 'system'} onValueChange={(v) => v && setTheme(v)}>
|
||||||
<SelectTrigger id="theme-select" className="w-full max-w-xs" />
|
<SelectTrigger id="theme-select" className="w-full max-w-xs">
|
||||||
|
<SelectValue placeholder="Выберите тему" />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="light">Светлая</SelectItem>
|
<SelectItem value="light">Светлая</SelectItem>
|
||||||
<SelectItem value="dark">Тёмная</SelectItem>
|
<SelectItem value="dark">Тёмная</SelectItem>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
} from '@evobgp/ui/components/select'
|
} from '@evobgp/ui/components/select'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs'
|
||||||
import {
|
import {
|
||||||
@@ -251,7 +252,7 @@ function TenantSettingsComponent() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
{runtimeLogsForm.runtime_logs_auto_enabled === 'true' ? 'Вкл' : 'Выкл'}
|
<SelectValue placeholder="Выберите" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="true">Вкл</SelectItem>
|
<SelectItem value="true">Вкл</SelectItem>
|
||||||
@@ -308,11 +309,11 @@ function TenantSettingsComponent() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
{runtimeLogsForm.runtime_logs_auto_mode || 'Выберите'}
|
<SelectValue placeholder="Выберите" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="delete">Delete</SelectItem>
|
<SelectItem value="truncate">truncate — обнулить</SelectItem>
|
||||||
<SelectItem value="truncate">Truncate</SelectItem>
|
<SelectItem value="delete">delete — удалить файл</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="font-mono text-xs text-muted-foreground">
|
<p className="font-mono text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
cd /tmp
|
cd /tmp
|
||||||
rm -rf "bird-${BIRD_VERSION}"
|
rm -rf "bird-${BIRD_VERSION}"
|
||||||
curl -fsSL "https://bird.network.cz/download/bird-${BIRD_VERSION}.tar.gz" | tar xz
|
curl -fsSL "https://bird.nic.cz/download/bird-${BIRD_VERSION}.tar.gz" | tar xz
|
||||||
cd "bird-${BIRD_VERSION}"
|
cd "bird-${BIRD_VERSION}"
|
||||||
./configure --prefix=/usr/local --enable-client
|
./configure --prefix=/usr/local --enable-client
|
||||||
make -j"$(nproc)"
|
make -j"$(nproc)"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
FROM public.ecr.aws/docker/library/node:22-alpine AS deps
|
FROM public.ecr.aws/docker/library/node:22-alpine AS deps
|
||||||
WORKDIR /repo
|
WORKDIR /repo
|
||||||
RUN corepack enable
|
RUN corepack enable && corepack prepare [email protected] --activate
|
||||||
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
|
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
|
||||||
COPY apps/web/package.json ./apps/web/
|
COPY apps/web/package.json ./apps/web/
|
||||||
COPY packages/ui/package.json ./packages/ui/
|
COPY packages/ui/package.json ./packages/ui/
|
||||||
|
|||||||
+40
-4
@@ -184,6 +184,10 @@ type Registry struct {
|
|||||||
onTerminal func(j *Job)
|
onTerminal func(j *Job)
|
||||||
onEnqueued func(j *Job)
|
onEnqueued func(j *Job)
|
||||||
onRunning func(j *Job)
|
onRunning func(j *Job)
|
||||||
|
// inflightRefresh counts refresh-kind jobs (module_refresh, tenant_refresh) per tenant that
|
||||||
|
// have been enqueued but not yet finalized in finishModuleRefreshSuccess. Used for deterministic
|
||||||
|
// deploy coalescing under tenantRefreshMu (instead of polling job statuses).
|
||||||
|
inflightRefresh map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
type idempoKey struct {
|
type idempoKey struct {
|
||||||
@@ -194,10 +198,11 @@ type idempoKey struct {
|
|||||||
func NewRegistry(workerStart func(j *Job)) *Registry {
|
func NewRegistry(workerStart func(j *Job)) *Registry {
|
||||||
maxWorkers := registryMaxConcurrentJobs()
|
maxWorkers := registryMaxConcurrentJobs()
|
||||||
return &Registry{
|
return &Registry{
|
||||||
byID: make(map[string]*Job),
|
byID: make(map[string]*Job),
|
||||||
byIdempo: make(map[idempoKey]*Job),
|
byIdempo: make(map[idempoKey]*Job),
|
||||||
workerStart: workerStart,
|
workerStart: workerStart,
|
||||||
workerSem: make(chan struct{}, maxWorkers),
|
workerSem: make(chan struct{}, maxWorkers),
|
||||||
|
inflightRefresh: make(map[string]int),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +345,9 @@ func (r *Registry) Enqueue(tenantID, kind string, idempotencyKey *string, module
|
|||||||
r.byIdempo[idempoKey{tenant: tenantID, key: *idempotencyKey}] = j
|
r.byIdempo[idempoKey{tenant: tenantID, key: *idempotencyKey}] = j
|
||||||
}
|
}
|
||||||
r.byID[j.ID] = j
|
r.byID[j.ID] = j
|
||||||
|
if isRefreshKind(kind) {
|
||||||
|
r.inflightRefresh[tenantID]++
|
||||||
|
}
|
||||||
r.pruneTerminalIfOver(maxJobs)
|
r.pruneTerminalIfOver(maxJobs)
|
||||||
enqueuedHook := r.onEnqueued
|
enqueuedHook := r.onEnqueued
|
||||||
workerStart := r.workerStart
|
workerStart := r.workerStart
|
||||||
@@ -474,6 +482,34 @@ func (r *Registry) CountOtherActiveRefresh(tenantID, excludeJobID string) int {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isRefreshKind reports whether a job kind participates in deploy coalescing.
|
||||||
|
func isRefreshKind(kind string) bool {
|
||||||
|
return kind == KindModuleRefresh || kind == KindTenantRefresh
|
||||||
|
}
|
||||||
|
|
||||||
|
// finalizeRefreshCoalesce is called from finishModuleRefreshSuccess under tenantRefreshMu.
|
||||||
|
// It atomically decrements the per-tenant inflight refresh counter and reports whether the
|
||||||
|
// caller is the last outstanding refresh for the tenant (and therefore should render+deploy).
|
||||||
|
//
|
||||||
|
// Unlike CountOtherActiveRefresh (which polls job statuses and races under -race), this counter
|
||||||
|
// is incremented in Enqueue under r.mu and decremented here, so the "last one" decision is
|
||||||
|
// deterministic regardless of how fast each refresh's ingest completes.
|
||||||
|
func (r *Registry) finalizeRefreshCoalesce(tenantID string) bool {
|
||||||
|
if r == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
n := r.inflightRefresh[tenantID]
|
||||||
|
if n <= 1 {
|
||||||
|
// Last (or already-balanced to zero) — clear the slot and let the caller deploy.
|
||||||
|
delete(r.inflightRefresh, tenantID)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
r.inflightRefresh[tenantID] = n - 1
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func parseCursor(s string, off *int) error {
|
func parseCursor(s string, off *int) error {
|
||||||
_, err := fmt.Sscanf(s, "%d", off)
|
_, err := fmt.Sscanf(s, "%d", off)
|
||||||
return err
|
return err
|
||||||
|
|||||||
+60
-23
@@ -119,26 +119,7 @@ func (w *Worker) Process(j *Job) {
|
|||||||
|
|
||||||
switch j.Kind {
|
switch j.Kind {
|
||||||
case KindModuleRefresh:
|
case KindModuleRefresh:
|
||||||
mid, _ := j.Meta["module_id"].(string)
|
w.runModuleRefresh(j)
|
||||||
if strings.TrimSpace(mid) == "" {
|
|
||||||
j.Fail("missing module_id in job meta")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx, cancel := j.workContext()
|
|
||||||
defer cancel()
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
j.MarkCancelled()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := pipeline.RefreshModuleIngest(ctx, w.Store, w.httpClient(), j.TenantID, mid); err != nil {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
j.MarkCancelled()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
j.Fail(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.finishModuleRefreshSuccess(j, mid)
|
|
||||||
case KindTenantRefresh:
|
case KindTenantRefresh:
|
||||||
w.runTenantRefresh(j)
|
w.runTenantRefresh(j)
|
||||||
case KindPeerReconcile:
|
case KindPeerReconcile:
|
||||||
@@ -295,7 +276,55 @@ func (w *Worker) tenantRefreshMu(tenantID string) *sync.Mutex {
|
|||||||
|
|
||||||
// finishModuleRefreshSuccess marks the refresh job and, for the last active refresh in tenant,
|
// finishModuleRefreshSuccess marks the refresh job and, for the last active refresh in tenant,
|
||||||
// creates one aggregate revision and enqueues a single deploy_apply.
|
// creates one aggregate revision and enqueues a single deploy_apply.
|
||||||
|
// runModuleRefresh handles a single module_refresh job and guarantees the per-tenant inflight
|
||||||
|
// slot is released exactly once — even on failure/cancellation before finishModuleRefreshSuccess.
|
||||||
|
func (w *Worker) runModuleRefresh(j *Job) {
|
||||||
|
coalesceFinalized := false
|
||||||
|
defer func() {
|
||||||
|
if !coalesceFinalized && w != nil && w.Registry != nil {
|
||||||
|
// Refresh failed/was cancelled before reaching finishModuleRefreshSuccess.
|
||||||
|
// Decrement the counter under the tenant mutex so the "last one" logic stays sound.
|
||||||
|
mu := w.tenantRefreshMu(j.TenantID)
|
||||||
|
mu.Lock()
|
||||||
|
w.Registry.finalizeRefreshCoalesce(j.TenantID)
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
mid, _ := j.Meta["module_id"].(string)
|
||||||
|
if strings.TrimSpace(mid) == "" {
|
||||||
|
j.Fail("missing module_id in job meta")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := j.workContext()
|
||||||
|
defer cancel()
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
j.MarkCancelled()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := pipeline.RefreshModuleIngest(ctx, w.Store, w.httpClient(), j.TenantID, mid); err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
j.MarkCancelled()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
j.Fail(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.finishModuleRefreshSuccess(j, mid)
|
||||||
|
coalesceFinalized = true
|
||||||
|
}
|
||||||
|
|
||||||
func (w *Worker) runTenantRefresh(j *Job) {
|
func (w *Worker) runTenantRefresh(j *Job) {
|
||||||
|
coalesceFinalized := false
|
||||||
|
defer func() {
|
||||||
|
if !coalesceFinalized && w != nil && w.Registry != nil {
|
||||||
|
mu := w.tenantRefreshMu(j.TenantID)
|
||||||
|
mu.Lock()
|
||||||
|
w.Registry.finalizeRefreshCoalesce(j.TenantID)
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
moduleIDs := moduleIDsFromJobMeta(j.Meta)
|
moduleIDs := moduleIDsFromJobMeta(j.Meta)
|
||||||
if len(moduleIDs) == 0 {
|
if len(moduleIDs) == 0 {
|
||||||
j.Fail("missing module_ids in job meta")
|
j.Fail("missing module_ids in job meta")
|
||||||
@@ -318,6 +347,7 @@ func (w *Worker) runTenantRefresh(j *Job) {
|
|||||||
}
|
}
|
||||||
j.mergeMeta(map[string]any{"module_ids": moduleIDs, "modules_refreshed": len(moduleIDs)})
|
j.mergeMeta(map[string]any{"module_ids": moduleIDs, "modules_refreshed": len(moduleIDs)})
|
||||||
w.finishModuleRefreshSuccess(j, trigger)
|
w.finishModuleRefreshSuccess(j, trigger)
|
||||||
|
coalesceFinalized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func moduleIDsFromJobMeta(meta map[string]any) []string {
|
func moduleIDsFromJobMeta(meta map[string]any) []string {
|
||||||
@@ -346,6 +376,9 @@ func moduleIDsFromJobMeta(meta map[string]any) []string {
|
|||||||
|
|
||||||
func (w *Worker) finishModuleRefreshSuccess(j *Job, triggerModuleID string) {
|
func (w *Worker) finishModuleRefreshSuccess(j *Job, triggerModuleID string) {
|
||||||
if w == nil || w.Store == nil {
|
if w == nil || w.Store == nil {
|
||||||
|
if w != nil && w.Registry != nil {
|
||||||
|
w.Registry.finalizeRefreshCoalesce(j.TenantID)
|
||||||
|
}
|
||||||
j.Succeed()
|
j.Succeed()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -354,11 +387,15 @@ func (w *Worker) finishModuleRefreshSuccess(j *Job, triggerModuleID string) {
|
|||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
ctx, cancel := j.workContext()
|
ctx, cancel := j.workContext()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
deferDeploy := false
|
// Determine whether this is the last outstanding refresh for the tenant. The counter is
|
||||||
|
// incremented in Enqueue (under r.mu) and decremented here, so the "last one" decision is
|
||||||
|
// deterministic regardless of ingest timing — unlike the previous status-polling approach
|
||||||
|
// (CountOtherActiveRefresh) which could race under -race.
|
||||||
|
isLastRefresh := true
|
||||||
if w.Registry != nil {
|
if w.Registry != nil {
|
||||||
deferDeploy = w.Registry.CountOtherActiveRefresh(j.TenantID, j.ID) > 0
|
isLastRefresh = w.Registry.finalizeRefreshCoalesce(j.TenantID)
|
||||||
}
|
}
|
||||||
if deferDeploy {
|
if !isLastRefresh {
|
||||||
j.mergeMeta(map[string]any{
|
j.mergeMeta(map[string]any{
|
||||||
"deploy_apply_deferred": true,
|
"deploy_apply_deferred": true,
|
||||||
"deploy_apply_defer_reason": "parallel_module_refresh",
|
"deploy_apply_defer_reason": "parallel_module_refresh",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "evobgp-release",
|
"name": "evobgp-release",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "[email protected]",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm --filter @evobgp/web dev",
|
"dev": "pnpm --filter @evobgp/web dev",
|
||||||
"build": "pnpm --filter @evobgp/web build",
|
"build": "pnpm --filter @evobgp/web build",
|
||||||
|
|||||||
Reference in New Issue
Block a user