This is an automated email from the ASF dual-hosted git repository.

jason810496 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 529f429400a Go SDK: reject a task handler and a Dag that share a 
dag_id (#73876)
529f429400a is described below

commit 529f429400a3311590217622f86223691c3c4e6f
Author: PoAn Yang <[email protected]>
AuthorDate: Thu Oct 1 20:45:18 2026 +0900

    Go SDK: reject a task handler and a Dag that share a dag_id (#73876)
    
    Signed-off-by: PoAn Yang <[email protected]>
---
 go-sdk/airflow/bundle.go      |  46 +++++++++++++++--
 go-sdk/airflow/bundle_test.go | 111 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 154 insertions(+), 3 deletions(-)

diff --git a/go-sdk/airflow/bundle.go b/go-sdk/airflow/bundle.go
index 697b6b0ae7d..8f82b071211 100644
--- a/go-sdk/airflow/bundle.go
+++ b/go-sdk/airflow/bundle.go
@@ -31,7 +31,11 @@ import (
 type BundleRef struct {
        // closed ends registration for everything the bundle can hold, so a 
kind added later
        // is covered without a flag of its own. Serve sets it; Register reads 
it.
-       closed       atomic.Bool
+       closed atomic.Bool
+       // mu is the lock for writes to taskHandlers and dags. Register holds 
it for the whole call,
+       // so two concurrent calls cannot register a task handler and a Dag 
with the same dag_id.
+       // Readers such as LookupTask take only the lock of the map they read.
+       mu           sync.Mutex
        taskHandlers taskHandlerMap
        dags         dagMap
 }
@@ -76,22 +80,42 @@ type Registerable interface{ registerable() }
 // registered.
 //
 // Register panics if a task handler with the same dag_id and task_id is 
already registered,
-// if a Dag with the same dag_id is already registered, or if 
[BundleRef.Serve] has already
-// been called: registration closes when serving starts.
+// if a Dag with the same dag_id is already registered, if a task handler and 
a Dag have the
+// same dag_id, or if [BundleRef.Serve] has already been called: registration 
closes when
+// serving starts. A task handler runs a task of a Python Dag, so its dag_id 
cannot also belong
+// to a Dag authored in Go.
 func (b *BundleRef) Register(items ...Registerable) {
        if b.closed.Load() {
                panic(
                        "airflow.BundleRef.Register: Serve has already been 
called; register everything before Serve",
                )
        }
+       b.mu.Lock()
+       defer b.mu.Unlock()
        for _, item := range items {
                switch item := item.(type) {
                case *taskHandler:
+                       if b.dags.has(item.dagID) {
+                               panic(fmt.Sprintf(
+                                       "airflow.BundleRef.Register: Dag %q is 
already registered as a Dag from "+
+                                               "airflow.Dag, so it cannot also 
have task handlers from "+
+                                               "airflow.TaskHandler",
+                                       item.dagID,
+                               ))
+                       }
                        b.taskHandlers.add(item.dagID, item.taskID, item.task)
                case *DagRef:
                        if item == nil {
                                panic("airflow.BundleRef.Register: cannot 
register a nil *airflow.DagRef")
                        }
+                       if b.taskHandlers.hasDag(item.dagID) {
+                               panic(fmt.Sprintf(
+                                       "airflow.BundleRef.Register: Dag %q 
already has task handlers from "+
+                                               "airflow.TaskHandler, so it 
cannot also be registered as a Dag from "+
+                                               "airflow.Dag",
+                                       item.dagID,
+                               ))
+                       }
                        b.dags.add(item)
                default:
                        // Either a nil item, or a struct from another package 
that embeds a Registerable.
@@ -135,6 +159,14 @@ func (m *taskHandlerMap) add(dagID, taskID string, task 
bundle.Task) {
        m.order = append(m.order, bundle.TaskHandlerInfo{DagID: dagID, TaskID: 
taskID})
 }
 
+func (m *taskHandlerMap) hasDag(dagID string) bool {
+       m.mu.RLock()
+       defer m.mu.RUnlock()
+
+       _, exists := m.handlers[dagID]
+       return exists
+}
+
 func (m *taskHandlerMap) LookupTask(dagID, taskID string) (bundle.Task, bool) {
        m.mu.RLock()
        defer m.mu.RUnlock()
@@ -169,3 +201,11 @@ func (m *dagMap) add(dag *DagRef) {
        }
        m.dags[dag.dagID] = dag
 }
+
+func (m *dagMap) has(dagID string) bool {
+       m.mu.Lock()
+       defer m.mu.Unlock()
+
+       _, exists := m.dags[dagID]
+       return exists
+}
diff --git a/go-sdk/airflow/bundle_test.go b/go-sdk/airflow/bundle_test.go
index d61acbf0454..31a654c453f 100644
--- a/go-sdk/airflow/bundle_test.go
+++ b/go-sdk/airflow/bundle_test.go
@@ -143,6 +143,80 @@ func TestRegisterRejectsDuplicateDag(t *testing.T) {
        assert.False(t, second.registered, "a Dag that Register rejects can 
still take tasks")
 }
 
+func TestRegisterRejectsADagWithTheDagIDOfATaskHandler(t *testing.T) {
+       tests := []struct {
+               name     string
+               register func(b *BundleRef, handler Registerable, dag *DagRef)
+       }{
+               {
+                       name: "separate calls",
+                       register: func(b *BundleRef, handler Registerable, dag 
*DagRef) {
+                               b.Register(handler)
+                               b.Register(dag)
+                       },
+               },
+               {
+                       name: "one call",
+                       register: func(b *BundleRef, handler Registerable, dag 
*DagRef) {
+                               b.Register(handler, dag)
+                       },
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       b := Bundle()
+                       dag := Dag("etl")
+
+                       want := `airflow.BundleRef.Register: Dag "etl" already 
has task handlers from ` +
+                               `airflow.TaskHandler, so it cannot also be 
registered as a Dag from airflow.Dag`
+                       assert.PanicsWithValue(t, want, func() {
+                               tt.register(b, TaskHandler("etl", "transform", 
noop), dag)
+                       })
+                       _, ok := b.taskHandlers.LookupTask("etl", "transform")
+                       assert.True(t, ok)
+                       assert.NotContains(t, b.dags.dags, "etl")
+                       assert.NotPanics(t, func() { dag.Task(extract) },
+                               "a Dag that Register rejects can still take 
tasks")
+               })
+       }
+}
+
+func TestRegisterRejectsATaskHandlerWithTheDagIDOfADag(t *testing.T) {
+       tests := []struct {
+               name     string
+               register func(b *BundleRef, handler Registerable, dag *DagRef)
+       }{
+               {
+                       name: "separate calls",
+                       register: func(b *BundleRef, handler Registerable, dag 
*DagRef) {
+                               b.Register(dag)
+                               b.Register(handler)
+                       },
+               },
+               {
+                       name: "one call",
+                       register: func(b *BundleRef, handler Registerable, dag 
*DagRef) {
+                               b.Register(dag, handler)
+                       },
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       b := Bundle()
+                       dag := Dag("etl")
+
+                       want := `airflow.BundleRef.Register: Dag "etl" is 
already registered as a Dag from ` +
+                               `airflow.Dag, so it cannot also have task 
handlers from airflow.TaskHandler`
+                       assert.PanicsWithValue(t, want, func() {
+                               tt.register(b, TaskHandler("etl", "transform", 
noop), dag)
+                       })
+                       assert.Same(t, dag, b.dags.dags["etl"])
+                       assert.NotContains(t, b.taskHandlers.handlers, "etl")
+                       assert.Empty(t, b.taskHandlers.ListTaskHandlers())
+               })
+       }
+}
+
 func TestRegisterRejectsNilDag(t *testing.T) {
        var dag *DagRef
        assert.PanicsWithValue(t, "airflow.BundleRef.Register: cannot register 
a nil *airflow.DagRef",
@@ -204,6 +278,43 @@ func TestRegisterIsSafeForConcurrentUse(t *testing.T) {
        assert.Len(t, b.dags.dags, workers*perWorker)
 }
 
+// For each dag_id, one goroutine registers a task handler and another 
registers a Dag at the
+// same time, and exactly one of the two calls must succeed. Without 
BundleRef.mu both can
+// succeed. A run with -race does not report that, because every map access 
still takes the
+// map's lock.
+func TestRegisterGivesEachDagIDToOneKindUnderConcurrentUse(t *testing.T) {
+       const dagCount = 200
+
+       b := Bundle()
+       start := make(chan struct{})
+       var wg sync.WaitGroup
+       register := func(item Registerable) {
+               defer wg.Done()
+               defer func() { recover() }()
+               <-start
+               b.Register(item)
+       }
+       dags := make([]*DagRef, dagCount)
+       for i := range dags {
+               dags[i] = Dag(fmt.Sprintf("dag_%d", i))
+               wg.Add(2)
+               go register(TaskHandler(dags[i].dagID, "transform", noop))
+               go register(dags[i])
+       }
+       close(start)
+       wg.Wait()
+
+       var wrong []string
+       for _, dag := range dags {
+               _, hasHandler := b.taskHandlers.LookupTask(dag.dagID, 
"transform")
+               if hasHandler == dag.registered {
+                       wrong = append(wrong, dag.dagID)
+               }
+       }
+       assert.Empty(t, wrong,
+               "each dag_id must end up with a task handler or a Dag, not both 
or neither")
+}
+
 func TestRegisterRejectsNilItem(t *testing.T) {
        var item Registerable
        assert.PanicsWithValue(t, "airflow.BundleRef.Register: cannot register 
<nil>", func() {

Reply via email to