Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/added-18091-connector-hotload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Knowledge connectors added, edited, enabled, disabled or removed from the Knowledge Integrations dashboard now take effect without restarting the hive: a hive booted with no connectors builds and starts the syncer on the first save (Check, Sync and Connect stop answering `no knowledge connectors are configured`), and a running syncer is reconfigured in place so unchanged connectors keep syncing. The `knowledge.publish` mirror still needs a restart when its connector changes ([#18091](https://github.com/hivecommons/hive/issues/18091)).
21 changes: 13 additions & 8 deletions src/cmd/hive/boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,19 @@ type boot struct {
saved *snapshot.PersistedState

// bootKnowledge
gitSyncer *knowledge.GitSyncer
knowledgeConnectors *connector.Syncer
inboxSvc *inbox.Service
knowledgePublish *connector.Mirror
promotionScheduler *knowledge.PromotionScheduler
quotaAccount string
quotaPoolDir string
explicitQuotaPoolDir bool
gitSyncer *knowledge.GitSyncer
knowledgeConnectors *connector.Syncer
// knowledgeConnectorsMu guards knowledgeConnectors once the dashboard
// can hot-load it (#18091); startConnectorSyncer is the boot starter
// reused for a syncer first built after boot.
knowledgeConnectorsMu sync.Mutex
startConnectorSyncer func(ctx context.Context, s *connector.Syncer)
inboxSvc *inbox.Service
knowledgePublish *connector.Mirror
promotionScheduler *knowledge.PromotionScheduler
quotaAccount string
quotaPoolDir string
explicitQuotaPoolDir bool
// knowledgeAPIFallback records that knowledgeAPI is the file-only API
// bootKnowledge auto-enables when knowledge.enabled is false, not one
// built from the configured layers/engine/curator (#9231).
Expand Down
34 changes: 34 additions & 0 deletions src/cmd/hive/boot_knowledge_connectors.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func (b *boot) bootKnowledgeConnectors(deps bootKnowledgeDeps) {
// answers 200 with an empty list and the panel renders its empty state
// (#17918) instead of waiting forever on a 404.
b.bootInbox(knowledge.BaseDir())
b.startConnectorSyncer = deps.startConnectorSyncer
s, err := newKnowledgeConnectorSyncer(b.cfg.Knowledge.Connectors, nil, b.knowledgeAPI, knowledge.BaseDir(), b.logger, b.inboxAfterSync)
if err != nil {
b.logger.Warn("knowledge connectors disabled", "error", err)
Expand All @@ -101,17 +102,50 @@ func (b *boot) bootKnowledgeConnectors(deps bootKnowledgeDeps) {
if s == nil {
return
}
b.knowledgeConnectorsMu.Lock()
b.knowledgeConnectors = s
b.knowledgeConnectorsMu.Unlock()
if deps.startConnectorSyncer != nil {
deps.startConnectorSyncer(b.ctx, s)
}
b.logger.Info("knowledge connectors started", "count", len(b.cfg.Knowledge.Connectors))
}

// reloadKnowledgeConnectors applies a knowledge.connectors list saved from
// the dashboard to the running hive (#18091). With no syncer yet (the hive
// booted with zero connectors, or the boot build failed) it builds and
// starts one from the same inputs as bootKnowledgeConnectors; otherwise it
// reconfigures the running syncer in place, so unchanged connectors keep
// syncing undisturbed. The knowledge.publish mirror is not rebuilt: a
// changed publish connector still needs a restart.
func (b *boot) reloadKnowledgeConnectors(entries []config.KnowledgeConnector) (dashboard.KnowledgeConnectorRuntime, error) {
b.knowledgeConnectorsMu.Lock()
defer b.knowledgeConnectorsMu.Unlock()
if b.knowledgeConnectors != nil {
if err := b.knowledgeConnectors.Reconfigure(connectorConfigs(entries)); err != nil {
return nil, err
}
b.logger.Info("knowledge connectors reloaded", "count", len(entries))
return b.knowledgeConnectors, nil
}
s, err := newKnowledgeConnectorSyncer(entries, nil, b.knowledgeAPI, knowledge.BaseDir(), b.logger, b.inboxAfterSync)
if err != nil || s == nil {
return nil, err
}
b.knowledgeConnectors = s
if b.startConnectorSyncer != nil {
b.startConnectorSyncer(b.ctx, s)
}
b.logger.Info("knowledge connectors started", "count", len(entries), "hot_loaded", true)
return s, nil
}

// knowledgeConnectorRuntime exposes the syncer to the dashboard. A nil
// *connector.Syncer must stay a nil interface so the dashboard reports
// "no connectors configured" instead of calling methods on a nil pointer.
func (b *boot) knowledgeConnectorRuntime() dashboard.KnowledgeConnectorRuntime {
b.knowledgeConnectorsMu.Lock()
defer b.knowledgeConnectorsMu.Unlock()
if b.knowledgeConnectors == nil {
return nil
}
Expand Down
52 changes: 52 additions & 0 deletions src/cmd/hive/boot_knowledge_connectors_reload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"context"
"testing"

"github.com/hivecommons/hive/pkg/config"
"github.com/hivecommons/hive/pkg/knowledge"
"github.com/hivecommons/hive/pkg/knowledge/connector"
)

// A hive booted with zero connectors builds and starts the syncer the first
// time the dashboard saves one, then reconfigures that same syncer (#18091).
func TestReloadKnowledgeConnectors(t *testing.T) {
knowledge.SetBaseDirForTest(t, t.TempDir())
b, log := newDepsTestBoot(t, &config.Config{})
var started []*connector.Syncer
b.bootKnowledgeConnectors(bootKnowledgeDeps{startConnectorSyncer: func(_ context.Context, s *connector.Syncer) {
started = append(started, s)
}})
if b.knowledgeConnectorRuntime() != nil || len(started) != 0 {
t.Fatal("no connectors at boot must leave the syncer off")
}

if rt, err := b.reloadKnowledgeConnectors(nil); rt != nil || err != nil {
t.Fatalf("reload with none = %v, %v; want nil, nil", rt, err)
}

docs := config.KnowledgeConnector{Name: "docs", Type: connector.TypeDocument, Layer: "project",
Scope: map[string]string{"url": "https://example.org/guide.html"}}
rt, err := b.reloadKnowledgeConnectors([]config.KnowledgeConnector{docs})
if err != nil || rt == nil {
t.Fatalf("reload = %v, %v\n%s", rt, err, log.String())
}
if len(started) != 1 || started[0] != b.knowledgeConnectors {
t.Fatalf("hot-loaded syncer not started once: %d", len(started))
}
if sts := rt.Statuses(); len(sts) != 1 || sts[0].Name != "docs" {
t.Fatalf("statuses = %+v", sts)
}

rt2, err := b.reloadKnowledgeConnectors(nil)
if err != nil || rt2 != rt {
t.Fatalf("second reload = %v, %v; want the same runtime", rt2, err)
}
if sts := rt2.Statuses(); len(sts) != 0 {
t.Fatalf("removed connector still listed: %+v", sts)
}
if len(started) != 1 {
t.Fatalf("reconfigure must not start a second syncer: %d", len(started))
}
}
25 changes: 13 additions & 12 deletions src/cmd/hive/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1140,18 +1140,19 @@ func (b *boot) wireBootClosures() {
// #3972: hand the ACMM advisor the SAME cached fleet-stats collector
// the heartbeat reads, so its merge-success signal reuses the existing
// 30-minute collect loop instead of issuing a second GitHub fetch.
FleetStats: b.fleetStatsCollector,
Activity: b.activityCollector,
RepoCost: b.repoCostCollector,
LaunchCost: b.launchCostCollector,
BeadSynthesizer: b.beadSynth,
KnowledgeConnectors: b.knowledgeConnectorRuntime(),
KnowledgePublish: b.knowledgePublishRuntime(),
Inbox: b.inboxSvc,
BeadStores: b.beadStores,
BeadStoreLoadFailures: b.beadStoreLoadFailures,
AuditLedger: auditLedger,
AuditJournal: auditJournal,
FleetStats: b.fleetStatsCollector,
Activity: b.activityCollector,
RepoCost: b.repoCostCollector,
LaunchCost: b.launchCostCollector,
BeadSynthesizer: b.beadSynth,
KnowledgeConnectors: b.knowledgeConnectorRuntime(),
ReloadKnowledgeConnectors: b.reloadKnowledgeConnectors,
KnowledgePublish: b.knowledgePublishRuntime(),
Inbox: b.inboxSvc,
BeadStores: b.beadStores,
BeadStoreLoadFailures: b.beadStoreLoadFailures,
AuditLedger: auditLedger,
AuditJournal: auditJournal,
AuditPublisherFunc: func() convergenceaudit.FindingPublisher {
return b.findingPublisher
},
Expand Down
6 changes: 3 additions & 3 deletions src/docs/api-reference/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ Part of the [Dashboard REST API reference](../api-reference.md); see that page f
| `GET` | `/api/openrouter/models` | Dashboard auth/session | Open Router Models | `pkg/dashboard/openrouter.go:47` |
| `GET` | `/api/openrouter/credit` | Dashboard auth/session | Open Router Credit | `pkg/dashboard/openrouter.go:48` |
| `GET` | `/openrouter/callback` | Public | Open Router Callback | `pkg/dashboard/openrouter.go:49` |
| `POST` | `/api/github-app/recheck` | Dashboard auth/session | GitHub App Recheck | `pkg/dashboard/server.go:1712` |
| `POST` | `/api/github-app/install-clicked` | Dashboard auth/session | GitHub App Install Clicked | `pkg/dashboard/server.go:1713` |
| `GET` | `/gh-setup` | Public | GitHub App Setup Callback | `pkg/dashboard/server.go:1714` |
| `POST` | `/api/github-app/recheck` | Dashboard auth/session | GitHub App Recheck | `pkg/dashboard/server.go:1716` |
| `POST` | `/api/github-app/install-clicked` | Dashboard auth/session | GitHub App Install Clicked | `pkg/dashboard/server.go:1717` |
| `GET` | `/gh-setup` | Public | GitHub App Setup Callback | `pkg/dashboard/server.go:1718` |
2 changes: 1 addition & 1 deletion src/docs/api-reference/dashboard-misc.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ The monitor renders CI and Review concurrently on one shared PR row; see
| `POST` | `/api/hives/{id}/heartbeat` | Dashboard auth/session | Hives Heartbeat | `pkg/dashboard/api_contribute.go:377` |
| `DELETE` | `/api/hives/{id}` | Owner only | Hives Delete | `pkg/dashboard/api_contribute.go:378` |
| `POST` | `/api/hives/onboard` | Dashboard auth/session | Hives Onboard | `pkg/dashboard/api_contribute.go:379` |
| `GET` | `/sso` | Public | SSO | `pkg/dashboard/server.go:1719` |
| `GET` | `/sso` | Public | SSO | `pkg/dashboard/server.go:1723` |
### Activity observation provenance

The CI snapshot retains real check/node IDs, head, App ID and transition times.
Expand Down
Loading
Loading