Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
60a0ba6
chore: log the stack trace when a queue consumer slice panics
medcl Oct 9, 2026
5872b3f
Merge branch 'main' of github.com:infinilabs/framework
medcl Oct 14, 2026
a05e575
feat(pipeline): per-step stats for sequential runs
medcl Oct 14, 2026
527a7d9
feat(pipeline): unified pipeline engine — records, contracts, lanes, …
Oct 14, 2026
460f4d1
chore: go.mod/go.sum updates for new dependencies
Oct 14, 2026
cb3e1cd
feat(pipeline): distributed deployment management (D7/P9 skeleton)
Oct 14, 2026
59d5c62
feat(pipeline): deployment reconciler pushes via configs-center
Oct 14, 2026
94fc347
feat(pipeline): P9 deployment status + instance runtime reporting
Oct 14, 2026
ce01a23
fix(pipeline): register ORM schemas for Pipeline models
Oct 14, 2026
55450e3
fix(pipeline): register PipelineConfigV2 ORM schema
Oct 14, 2026
04c362f
fix(pipeline): generate ID for PipelineDeployment on create
Oct 14, 2026
5a5b145
feat(pipeline): instance runtime reporting in configs _sync
Oct 14, 2026
c2a4929
feat(pipeline): instance-targeted deployment (selector)
Oct 14, 2026
d2e846b
fix(pipeline): output processor accepts string queue names
Oct 15, 2026
0771447
fix: skip deployment reconcile and guard ORM access on lightweight ag…
medcl Oct 15, 2026
14564cf
fix: deployment reconciler convergence and name-based status matching
medcl Oct 15, 2026
74832e7
feat: compile-time validation gate before pipeline configs are persis…
medcl Oct 15, 2026
82ad393
feat: per-processor constructor validation audit — no panics, require…
medcl Oct 15, 2026
7249fb9
feat: pipeline rollback gate and instance-side failure visibility
medcl Oct 15, 2026
eec327e
fix: unified output queue accepts bare name or full config object
medcl Oct 15, 2026
054f3f0
fix: mint the reverse-channel loopback API token agents were missing
medcl Oct 15, 2026
04ac08c
docs: pipeline validation gate, config/version/deployment APIs, recor…
medcl Oct 15, 2026
0659874
feat: deployment disable propagates to instances; delete cascades con…
medcl Oct 15, 2026
2fcbfc7
feat: deployment placement targeting with exact per-instance convergence
medcl Oct 15, 2026
3b64c19
feat(pipeline): add case/branches multi-way dispatch processor
medcl Oct 20, 2026
cabe55e
Merge remote-tracking branch 'gh/main' into refactoring-pipeline-oct
medcl Oct 21, 2026
0355665
feat: core and module support for lane-based bulk indexing
medcl Oct 21, 2026
c27efad
feat(queue): disk-queue consumer self-heal
medcl Oct 21, 2026
029fdc4
feat(configs): per-instance config rows shadow group defaults
medcl Oct 21, 2026
efcb929
feat(pipeline): lane-based bulk indexing processors
medcl Oct 21, 2026
29164e9
docs: processor reference pages for the lane bulk pipeline
medcl Oct 21, 2026
400c77d
fix(ci): keep the empty-config audit importable without the enterpris…
medcl Oct 21, 2026
9712739
ci: run unit tests with the enterprise plugins overlay
medcl Oct 21, 2026
2fb8dee
ci: fall back to DOCS_DEPLOYMENT_TOKEN until a dedicated CI_TOKEN exists
medcl Oct 21, 2026
86776ec
ci: quote the echo line — unquoted parens broke bash parsing
medcl Oct 21, 2026
0cc5c3c
docs: reference page for the case processor and index rows for if/case
medcl Oct 21, 2026
8fe4fcf
Merge origin/main (#434 glob rules in ReadGuard) into refactoring-pip…
medcl Oct 21, 2026
55b5f51
docs: keep code comments in English
medcl Oct 21, 2026
82e5fcc
docs: drop the README pipeline section in favor of release notes
medcl Oct 21, 2026
c3db6a4
Merge branch 'main' into refactoring-pipeline-oct
medcl Oct 22, 2026
c4ebe4a
feat(orm): body-level parameters on the query builder
medcl Oct 22, 2026
f2c2ee0
fix(pipeline): explicit PipelineConfigV2 mappings; replay error log fix
medcl Oct 22, 2026
2051f0f
feat(pipeline): count telemetry loss instead of dropping silently
medcl Oct 23, 2026
18633cc
fix(sqlite): must_not over a missing field drops rows under SQL three…
medcl Oct 23, 2026
95576cc
fix: access_token native path sets the ORM model so ES-backed manager…
medcl Oct 23, 2026
b6a3f0a
fix(ucfg): empty dict reifies to empty map so its key survives unpack
medcl Oct 23, 2026
f1ace01
fix(configs): placeholder version dates no longer kill instance heart…
medcl Oct 23, 2026
494c617
revert(pipeline): remove flight and persist loss accounting
medcl Oct 24, 2026
866b35e
fix(pipeline): contract enforcement actually reaches every processor
medcl Oct 24, 2026
1fea4be
feat(pipeline): record event pool, release wiring, P4 benchmarks (#67…
medcl Oct 24, 2026
e1845cc
fix(event): pooled back-reference as interface{} — typed cycle blew t…
medcl Oct 24, 2026
fd47c67
feat(pipeline): unified POST /pipeline/ai-generate with ChainGenerato…
medcl Oct 24, 2026
437d3c1
feat(pipeline): canvas_layout field on PipelineConfigV2 for UI node p…
medcl Oct 24, 2026
3806459
docs: lane reference with queue-vs-lane selection guide, pipeline glo…
medcl Oct 24, 2026
9f17a4a
feat(pipeline): for_each adopts the bound record in dry-run — sub-cha…
medcl Oct 24, 2026
539e914
refactor(pipeline): dry-run state rides the Go context chain
medcl Oct 24, 2026
c02da19
feat(pipeline): ORM pipeline edits hot-reload the local running task …
medcl Oct 24, 2026
3d75258
feat(pipeline): fleet control primitives — generation guard, drain, c…
medcl Oct 24, 2026
265b867
feat(pipeline): stage stats and step events carry instance_id (#127)
medcl Oct 24, 2026
68cb052
feat(pipeline): GET /pipeline/cluster/stages — cluster-wide stage tot…
medcl Oct 24, 2026
dc2e72b
feat(pipeline): fleet control plane — staged rollout, cluster broadca…
medcl Oct 24, 2026
2e4b5b5
feat: instance cordon flag reports over configs _sync (#128) and Requ…
medcl Oct 24, 2026
15b4249
feat(pipeline): deployment CRUD handlers poke the reconciler
medcl Oct 24, 2026
f9ae1f1
fix(pipeline): keep_running tasks report STARTED between passes
medcl Oct 24, 2026
14872ef
fix(pipeline): deployment status reports real last-seen, rollout read…
medcl Oct 24, 2026
0f84de3
feat(easysearch): EnabledByDefault opt-in for apps that own the clust…
medcl Oct 24, 2026
1e2ccfe
fix(pipeline): stage stats actually reach readers and survive securit…
medcl Oct 24, 2026
891adbc
feat(pipeline): stage stats ride the configs _sync report; retention …
medcl Oct 24, 2026
a7300c9
feat(pipeline): group-based deployment targeting via the placement se…
medcl Oct 25, 2026
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
66 changes: 66 additions & 0 deletions .github/workflows/unit_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,70 @@ jobs:
# for unit test
cd $WORK
echo Testing code at $PWD ...
OFFLINE_BUILD=true CI=true make tidy test

enterprise-test:
timeout-minutes: 60
runs-on: ubuntu-latest
env:
GO_VERSION: 1.23.4
steps:
- name: Checkout current repository
uses: actions/checkout@v4
with:
path: framework

- name: Set up go toolchain
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
check-latest: false
cache: true

# Enterprise plugins overlay (private repo): prefer the branch
# matching this PR's branch name, fall back to the default branch
# when it does not exist there; the license module is a build
# dependency of the overlay.
- name: Checkout enterprise plugins overlay
env:
CI_GH_TOKEN: ${{ secrets.CI_TOKEN || secrets.DOCS_DEPLOYMENT_TOKEN }}
OVERLAY_REF: ${{ github.head_ref }}
run: |
set -e
REPO="https://x-access-token:${CI_GH_TOKEN}@github.com/infinilabs/framework-enterprise-plugins.git"
if [ -n "$OVERLAY_REF" ] && git ls-remote --exit-code --heads "$REPO" "refs/heads/${OVERLAY_REF}" >/dev/null 2>&1; then
git clone --depth 1 --branch "${OVERLAY_REF}" "$REPO" framework/plugins/enterprise
else
git clone --depth 1 "$REPO" framework/plugins/enterprise
fi
git -C framework/plugins/enterprise log --oneline -1

- name: Checkout license module
uses: actions/checkout@v4
with:
token: ${{ secrets.CI_TOKEN || secrets.DOCS_DEPLOYMENT_TOKEN }}
repository: infinilabs/license
ref: main
path: license

- name: Check go toolchain
run: go version

- name: Unit test with enterprise overlay
env:
GOFLAGS: -tags=ci
run: |
echo Home path is $HOME
export WORKBASE=$HOME/go/src/infini.sh
export WORK=$WORKBASE/framework

# for test workspace
mkdir -p $HOME/go/src/
ln -s $GITHUB_WORKSPACE $WORKBASE

# the overlay's umbrella packages need the private infini.sh/license module (kept out of go.mod)
cd $WORK
go mod edit -replace infini.sh/license=../license

echo "Testing code at $PWD (with enterprise overlay) ..."
OFFLINE_BUILD=true CI=true make tidy test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ generated_*.go

# keystore runtime artifacts
core/elastic/.keystore/
.mimosa/
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# INFINI Framework

## Pipeline Terminology (glossary)

The data-processing subsystems use a fixed vocabulary. The short version:

| Term | Meaning |
|------|---------|
| **message** | A queue's unit of storage — opaque bytes. Queues never interpret payloads. |
| **batch** | The pipeline's native input: a slice of messages, the *batch scope* all historical processors operate on. |
| **record** | A typed, decoded event inside a record host's sub-chain — the *record scope*; mutable in place, never leaks downstream. |
| **record host** | A splitter processor that turns a batch into records and back: `for_each`, `lane_consumer`, or a custom host. |
| **codec** | Pluggable byte↔record converter (`otel` default, `coco_document`, ...). |
| **namespace** | A record family (`document`, `attachment`) used in compile-time contract checks. |
| **queue** | Durable at-least-once **byte** channel; cross-process safe. |
| **lane** | In-process at-most-once **record** channel (zero serialization, pointer hand-off). |

Full definitions and the queue-vs-lane selection guide live in the docs: [Glossary](docs/content.en/docs/references/glossary.md), [Pipeline Record Processing](docs/content.en/docs/references/pipeline_record.md), [Lane](docs/content.en/docs/references/lane.md).

## Requirements
- Go 1.21+

Expand Down
44 changes: 44 additions & 0 deletions core/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -971,3 +971,47 @@ func TestLoadFileExpandsConfigTemplatesPlain(t *testing.T) {
assert.Equal(t, out.Entry[0].Name, "my_entry_test_entry")
assert.Equal(t, out.Entry[0].Network.Binding, "127.0.0.1:8002")
}

// A processor entry with an empty body (`- echo: {}`) must keep its key
// through LoadPath + Unpack. Empty dicts used to reify to nil, which made
// the KEY vanish from map[string]interface{} targets and broke every
// config-file-loaded pipeline (managed instances saw "each processor must
// have exactly one action, but found 0 actions").
func TestLoadPathKeepsEmptyProcessorKeys(t *testing.T) {
dir := t.TempDir()
yml := "pipeline:\n" +
" - name: t\n" +
" auto_start: false\n" +
" processor:\n" +
" - echo: {}\n" +
" - node_stamp:\n" +
" field: x\n"
if err := os.WriteFile(filepath.Join(dir, "p.yml"), []byte(yml), 0o600); err != nil {
t.Fatal(err)
}

cfg, err := LoadPath(dir)
if err != nil {
t.Fatal(err)
}
child, err := cfg.Child("pipeline", -1)
if err != nil {
t.Fatal(err)
}
type rawPipeline struct {
Processor []map[string]interface{} `config:"processor"`
}
list := []rawPipeline{}
if err := child.Unpack(&list); err != nil {
t.Fatal(err)
}
if len(list) != 1 || len(list[0].Processor) != 2 {
t.Fatalf("unexpected unpack result: %#v", list)
}
if _, ok := list[0].Processor[0]["echo"]; !ok {
t.Fatalf("empty processor body lost its key: %#v", list[0].Processor[0])
}
if _, ok := list[0].Processor[1]["node_stamp"]; !ok {
t.Fatalf("node_stamp lost its key: %#v", list[0].Processor[1])
}
}
20 changes: 15 additions & 5 deletions core/elastic/cluster_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,25 @@ func HydrateClusterSecrets(cfg *ElasticsearchConfig) {
v, ok := loadClusterSecret(ClusterBasicAuthPasswordKey(cfg.ID))
if ok {
if v == "" {
log.Warnf("cluster [%s]'s basic auth password is empty: %v", cfg.ID)
log.Warnf("cluster [%s]'s basic auth password is empty", cfg.ID)
}
cfg.BasicAuth.Password = ucfg.SecretString(v)
} else {
log.Warnf("cluster [%s]'s basic auth password not found in the keystore: %v", cfg.ID)
log.Warnf("cluster [%s]'s basic auth password not found in the keystore", cfg.ID)
}
}
if !isRealSecret(cfg.Token.Get()) {
// Empty token = the cluster never had one (no-auth) — nothing to
// hydrate and nothing to warn about. Only a MASKED token (record says a
// token exists, value stashed elsewhere) requires the keystore lookup.
if tok := cfg.Token.Get(); tok != "" && !isRealSecret(tok) {
v, ok := loadClusterSecret(ClusterTokenKey(cfg.ID))
if ok {
if v == "" {
log.Warnf("cluster [%s]'s token is empty: %v", cfg.ID)
log.Warnf("cluster [%s]'s token is empty", cfg.ID)
}
cfg.Token = ucfg.SecretString(v)
} else {
log.Warnf("cluster [%s]'s token not found in the keystore: %v", cfg.ID)
log.Warnf("cluster [%s]'s token not found in the keystore", cfg.ID)
}
}
}
Expand Down Expand Up @@ -171,6 +174,13 @@ func isRealSecret(s string) bool {
return s != "" && s != ucfg.SecretShadowText
}

// IsRealSecret is the exported form of isRealSecret, for app-side code
// building credential payloads from ORM-loaded cluster records (deciding
// whether a hydrated value is safe to ship).
func IsRealSecret(s string) bool {
return isRealSecret(s)
}

func asMapStr(v interface{}) map[string]interface{} {
switch m := v.(type) {
case map[string]interface{}:
Expand Down
2 changes: 1 addition & 1 deletion core/elastic/cluster_secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
"os"
"testing"

"infini.sh/framework/core/model"
"infini.sh/framework/core/keystore"
"infini.sh/framework/core/model"
ucfg "infini.sh/framework/lib/go-ucfg"
)

Expand Down
18 changes: 11 additions & 7 deletions core/env/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@
package env

type Version struct {
VersionNumber string `json:"number,omitempty"`
FrameworkBuildCommitHash string `json:"framework_hash,omitempty"`
FrameworkVendorBuildCommitHash string `json:"vendor_hash,omitempty"`
BuildCommitHash string `json:"build_hash,omitempty"`
BuildDate string `json:"build_date,omitempty"`
BuildNumber string `json:"build_number,omitempty"`
EolDate string `json:"eol_date,omitempty"`
VersionNumber string `json:"number,omitempty" elastic_mapping:"number: { type: keyword }"`
FrameworkBuildCommitHash string `json:"framework_hash,omitempty" elastic_mapping:"framework_hash: { type: keyword }"`
FrameworkVendorBuildCommitHash string `json:"vendor_hash,omitempty" elastic_mapping:"vendor_hash: { type: keyword }"`
BuildCommitHash string `json:"build_hash,omitempty" elastic_mapping:"build_hash: { type: keyword }"`
// BuildDate/EolDate must map to keyword, never a dynamic date: binaries
// built without version ldflags report placeholders ("N/A"), which a
// date mapping rejects with document_parsing_exception — killing
// instance heartbeats on indices whose mapping predates this pin.
BuildDate string `json:"build_date,omitempty" elastic_mapping:"build_date: { type: keyword }"`
BuildNumber string `json:"build_number,omitempty" elastic_mapping:"build_number: { type: keyword }"`
EolDate string `json:"eol_date,omitempty" elastic_mapping:"eol_date: { type: keyword }"`
}

func (env *Env) GetVersionInfo() Version {
Expand Down
7 changes: 7 additions & 0 deletions core/event/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ type Event struct {
Meta util.MapStr `json:"-"`
Private interface{} `json:"-"` // for beats private use
TimeSeries bool `json:"-"` // true if the event contains timeseries data

// pooled links to the record-pool wrapper while the event came from
// AcquireEvent; managed by pool.go, never serialized. Held as
// interface{} on purpose: a typed *pooledEvent back-reference would
// create a recursive type (Event -> pooledEvent -> Event) and blow
// the reflection walkers (GetTagsByTagName) with a stack overflow.
pooled interface{}
}

type EventMetadata struct {
Expand Down
138 changes: 138 additions & 0 deletions core/event/pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright (C) INFINI Labs & INFINI LIMITED.
//
// The INFINI Framework is offered under the GNU Affero General Public License v3.0
// and as commercial software.
//
// For commercial licensing, contact us at:
// - Website: infinilabs.com
// - Email: hello#infini.ltd
//
// Open Source licensed under AGPL V3:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

/* ©INFINI, All Rights Reserved.
* mail: contact#infini.ltd */

package event

import (
"sync"
"sync/atomic"
"time"
)

// The record pool recycles *Event across record-mode batches: decode-heavy
// hosts (for_each) otherwise allocate a fresh Event, its field maps and the
// whole decoded tree per record only to drop them after encode.
//
// Contract:
// - AcquireEvent returns a zeroed event; Decode-style producers write into
// it as if it were freshly allocated.
// - ReleaseEvent returns the event to the pool. It must be called only
// after the record has been fully encoded (or dropped) AND no downstream
// processor retains a reference — the host owns the record lifetime.
// - ReleaseEvent is a no-op on events that did not come from the pool
// (codec- or clone-allocated), so a host can release every record
// unconditionally.
// - A double release is rejected atomically (second call returns false)
// instead of pooling the same event twice.
//
// Reset policy: scalar fields are zeroed; the hot maps (Fields, Metadata
// labels, Meta) are cleared in place — their bucket arrays are reused —
// unless they grew past maxRecycledEntries, in which case they are dropped
// so one giant record cannot pin memory in the pool forever.

// maxRecycledEntries caps in-place map reuse; bigger maps are released to
// the GC instead of living in the pool.
const maxRecycledEntries = 128

type pooledEvent struct {
live atomic.Bool
ev Event
}

var recordPool = sync.Pool{New: func() interface{} { return &pooledEvent{} }}

var (
poolAcquires atomic.Int64
poolReleases atomic.Int64
)

// AcquireEvent returns an event from the record pool (zeroed).
func AcquireEvent() *Event {
pe := recordPool.Get().(*pooledEvent)
pe.live.Store(true)
pe.ev.pooled = pe
poolAcquires.Add(1)
return &pe.ev
}

// ReleaseEvent recycles a pooled event; returns true when the event went
// back to the pool, false for non-pooled events or a rejected double
// release.
func ReleaseEvent(e *Event) bool {
if e == nil {
return false
}
pe, ok := e.pooled.(*pooledEvent)
if !ok {
return false
}
if !pe.live.CompareAndSwap(true, false) {
return false
}
e.resetForPool()
e.pooled = nil
poolReleases.Add(1)
recordPool.Put(pe)
return true
}

// PoolStats reports cumulative acquire/release counts; the difference is
// the number of pooled events currently in use (leak detection).
func PoolStats() (acquired, released int64) {
return poolAcquires.Load(), poolReleases.Load()
}

// resetForPool clears an event for reuse; see the pool doc for the
// map-retention policy.
func (e *Event) resetForPool() {
e.Fields = recycleMap(e.Fields)
e.Metadata.Labels = recycleMap(e.Metadata.Labels)
e.Meta = recycleMap(e.Meta)
e.Metadata.Category = ""
e.Metadata.Name = ""
e.Metadata.Version = ""
e.Metadata.Datatype = ""
e.Agent = nil
e.Private = nil
e.QueueName = ""
e.TimeSeries = false
e.Timestamp = time.Time{}
}

// recycleMap clears a map in place when it is small enough to be worth
// keeping, else drops it.
func recycleMap(m map[string]interface{}) map[string]interface{} {
if m == nil {
return nil
}
if len(m) > maxRecycledEntries {
return nil
}
for k := range m {
delete(m, k)
}
return m
}
Loading
Loading