Skip to content

Commit 1bdf869

Browse files
Use FUSE for bundle commands on serverless (#4450)
## Changes This PR updates the CLI to use the FUSE to read and write notebooks instead of the workspace APIs, when the DBR version is serverless 2.5+. This has the following benefits: 1. Much faster bundle deployments / summaries etc for DABs in the workspace. 2. Fixes commands like `bundle generate --bind` which were previously failing on DABs in the workspace due to both the workspace APIs and FUSE being used simultaneously. 3. Enables running tests on DBR. With the previous logic, running tests on DBR would fail because of mixing workspace API access and FUSE access. ## Tests Manually verified that both bundle deploy / bundle generate continue to work correctly on DBR. Also verified that `bundle generate --bind` works on DBR. Acceptance tests will be added once #4416 lands. ---- bundle generate --bind works, i.e. all notebooks are written successfully via FUSE: <img width="821" height="293" alt="Screenshot 2026-02-05 at 23 30 08" src="https://github.com/user-attachments/assets/55d61b57-39a7-4119-8275-f38df17c6aba" /> ---- bundle deploy works, for all sorts of notebooks, i.e. they are read correctly. <img width="819" height="232" alt="Screenshot 2026-02-05 at 23 30 15" src="https://github.com/user-attachments/assets/badc91ae-1fee-41e8-9429-4cc8b6d2be97" /> ---- This was tested and run on serverless `client.2.5` (standard v2). The UI does not allow me to select a minor version so I could not test other client.2 versions like 2.4. That's why we only use FUSE from 2.5 onwards. In practice this is fine because customers wil also likely be using `2.5` or above it they are on serverless.
1 parent 7602a35 commit 1bdf869

File tree

4 files changed

+227
-9
lines changed

4 files changed

+227
-9
lines changed

bundle/config/mutator/configure_wsfs.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ func (m *configureWSFS) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagno
3434
return nil
3535
}
3636

37+
// On serverless (client version 2.5+), use the native sync root directly via FUSE.
38+
// The FUSE provides capabilities for both reading and writing notebooks. It also
39+
// is much faster and enables running cloud tests on DBR, since otherwise the tests
40+
// fail with an AsyncFlushError because of the conflict between writing to FUSE
41+
// and via the workspace APIs simultaneously.
42+
//
43+
// Writing notebooks via FUSE is only supported for serverless client version 2+.
44+
// Since we could only test v2.5, since the platform only allows selecting v2 (which is v2.5 internally),
45+
// we restrict FUSE to only be used for v2.5+.
46+
v := dbr.GetVersion(ctx)
47+
if v.Type == dbr.ClusterTypeServerless && (v.Major > 2 || (v.Major == 2 && v.Minor >= 5)) {
48+
return nil
49+
}
50+
3751
// If so, swap out vfs.Path instance of the sync root with one that
3852
// makes all Workspace File System interactions extension aware.
3953
p, err := vfs.NewFilerPath(ctx, root, func(path string) (filer.Filer, error) {

bundle/config/mutator/configure_wsfs_test.go

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package mutator_test
22

33
import (
44
"context"
5+
"reflect"
56
"runtime"
67
"testing"
78

@@ -53,13 +54,55 @@ func TestConfigureWSFS_SkipsIfNotRunningOnRuntime(t *testing.T) {
5354
assert.Equal(t, originalSyncRoot, b.SyncRoot)
5455
}
5556

56-
func TestConfigureWSFS_SwapSyncRoot(t *testing.T) {
57-
b := mockBundleForConfigureWSFS(t, "/Workspace/foo")
58-
originalSyncRoot := b.SyncRoot
57+
func TestConfigureWSFS_DBRVersions(t *testing.T) {
58+
tests := []struct {
59+
name string
60+
version string
61+
expectFUSE bool // true = osPath (uses FUSE), false = filerPath (uses wsfs extension)
62+
}{
63+
// Serverless client version 2.5+ should use FUSE directly (osPath)
64+
{"serverless_client_2_5", "client.2.5", true},
65+
{"serverless_client_2_6", "client.2.6", true},
66+
{"serverless_client_3", "client.3", true},
67+
{"serverless_client_3_0", "client.3.0", true},
68+
{"serverless_client_3_6", "client.3.6", true},
69+
{"serverless_client_4_9", "client.4.9", true},
70+
{"serverless_client_4_10", "client.4.10", true},
5971

60-
ctx := context.Background()
61-
ctx = dbr.MockRuntime(ctx, dbr.Environment{IsDbr: true, Version: "15.4"})
62-
diags := bundle.Apply(ctx, b, mutator.ConfigureWSFS())
63-
assert.Empty(t, diags)
64-
assert.NotEqual(t, originalSyncRoot, b.SyncRoot)
72+
// Serverless client version < 2.5 should use wsfs extension client (filerPath)
73+
{"serverless_client_1", "client.1", false},
74+
{"serverless_client_1_13", "client.1.13", false},
75+
{"serverless_client_2", "client.2", false},
76+
{"serverless_client_2_0", "client.2.0", false},
77+
{"serverless_client_2_1", "client.2.1", false},
78+
{"serverless_client_2_4", "client.2.4", false},
79+
80+
// Interactive (non-serverless) versions should use wsfs extension client (filerPath)
81+
{"interactive_15_4", "15.4", false},
82+
{"interactive_16_3", "16.3", false},
83+
{"interactive_16_4", "16.4", false},
84+
{"interactive_17_0", "17.0", false},
85+
{"interactive_17_1", "17.1", false},
86+
{"interactive_17_2", "17.2", false},
87+
{"interactive_17_3", "17.3", false},
88+
}
89+
90+
for _, tt := range tests {
91+
t.Run(tt.name, func(t *testing.T) {
92+
b := mockBundleForConfigureWSFS(t, "/Workspace/foo")
93+
94+
ctx := context.Background()
95+
ctx = dbr.MockRuntime(ctx, dbr.Environment{IsDbr: true, Version: tt.version})
96+
diags := bundle.Apply(ctx, b, mutator.ConfigureWSFS())
97+
assert.Empty(t, diags)
98+
99+
// Check the underlying type of SyncRoot
100+
typeName := reflect.TypeOf(b.SyncRoot).String()
101+
if tt.expectFUSE {
102+
assert.Equal(t, "*vfs.osPath", typeName, "expected osPath (FUSE) for version %s", tt.version)
103+
} else {
104+
assert.Equal(t, "*vfs.filerPath", typeName, "expected filerPath (wsfs extension) for version %s", tt.version)
105+
}
106+
})
107+
}
65108
}

libs/dbr/context.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package dbr
22

3-
import "context"
3+
import (
4+
"context"
5+
"strconv"
6+
"strings"
7+
)
48

59
// key is a package-local type to use for context keys.
610
//
@@ -15,6 +19,71 @@ const (
1519
dbrKey = key(1)
1620
)
1721

22+
// ClusterType represents the type of Databricks cluster.
23+
type ClusterType int
24+
25+
const (
26+
ClusterTypeUnknown ClusterType = iota
27+
ClusterTypeInteractive
28+
ClusterTypeServerless
29+
)
30+
31+
func (t ClusterType) String() string {
32+
switch t {
33+
case ClusterTypeInteractive:
34+
return "interactive"
35+
case ClusterTypeServerless:
36+
return "serverless"
37+
default:
38+
return "unknown"
39+
}
40+
}
41+
42+
// Version represents a parsed DBR version.
43+
type Version struct {
44+
Type ClusterType
45+
Major int
46+
Minor int
47+
Raw string
48+
}
49+
50+
// ParseVersion parses a DBR version string and returns structured version info.
51+
// Examples:
52+
// - "16.3" -> Interactive, Major=16, Minor=3
53+
// - "client.4.9" -> Serverless, Major=4, Minor=9
54+
func ParseVersion(version string) Version {
55+
result := Version{Raw: version}
56+
57+
if version == "" {
58+
return result
59+
}
60+
61+
// Serverless versions have "client." prefix
62+
if strings.HasPrefix(version, "client.") {
63+
result.Type = ClusterTypeServerless
64+
// Parse "client.X.Y" format
65+
parts := strings.Split(strings.TrimPrefix(version, "client."), ".")
66+
if len(parts) >= 1 {
67+
result.Major, _ = strconv.Atoi(parts[0])
68+
}
69+
if len(parts) >= 2 {
70+
result.Minor, _ = strconv.Atoi(parts[1])
71+
}
72+
return result
73+
}
74+
75+
// Interactive versions are "X.Y" format
76+
result.Type = ClusterTypeInteractive
77+
parts := strings.Split(version, ".")
78+
if len(parts) >= 1 {
79+
result.Major, _ = strconv.Atoi(parts[0])
80+
}
81+
if len(parts) >= 2 {
82+
result.Minor, _ = strconv.Atoi(parts[1])
83+
}
84+
return result
85+
}
86+
1887
type Environment struct {
1988
IsDbr bool
2089
Version string
@@ -61,3 +130,9 @@ func RuntimeVersion(ctx context.Context) string {
61130

62131
return v.(Environment).Version
63132
}
133+
134+
// GetVersion returns the parsed runtime version from the context.
135+
// It expects a context returned by [DetectRuntime] or [MockRuntime].
136+
func GetVersion(ctx context.Context) Version {
137+
return ParseVersion(RuntimeVersion(ctx))
138+
}

libs/dbr/context_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,89 @@ func TestContext_RuntimeVersionWithMock(t *testing.T) {
8484
assert.Equal(t, "15.4", RuntimeVersion(MockRuntime(ctx, Environment{IsDbr: true, Version: "15.4"})))
8585
assert.Empty(t, RuntimeVersion(MockRuntime(ctx, Environment{})))
8686
}
87+
88+
func TestParseVersion_Serverless(t *testing.T) {
89+
tests := []struct {
90+
version string
91+
expectedType ClusterType
92+
expectedMajor int
93+
expectedMinor int
94+
}{
95+
{"client.4.9", ClusterTypeServerless, 4, 9},
96+
{"client.4.10", ClusterTypeServerless, 4, 10},
97+
{"client.3.6", ClusterTypeServerless, 3, 6},
98+
{"client.2", ClusterTypeServerless, 2, 0},
99+
{"client.2.1", ClusterTypeServerless, 2, 1},
100+
{"client.1", ClusterTypeServerless, 1, 0},
101+
{"client.1.13", ClusterTypeServerless, 1, 13},
102+
}
103+
104+
for _, tt := range tests {
105+
t.Run(tt.version, func(t *testing.T) {
106+
v := ParseVersion(tt.version)
107+
assert.Equal(t, tt.expectedType, v.Type)
108+
assert.Equal(t, tt.expectedMajor, v.Major)
109+
assert.Equal(t, tt.expectedMinor, v.Minor)
110+
assert.Equal(t, tt.version, v.Raw)
111+
})
112+
}
113+
}
114+
115+
func TestParseVersion_Interactive(t *testing.T) {
116+
tests := []struct {
117+
version string
118+
expectedType ClusterType
119+
expectedMajor int
120+
expectedMinor int
121+
}{
122+
{"16.3", ClusterTypeInteractive, 16, 3},
123+
{"16.4", ClusterTypeInteractive, 16, 4},
124+
{"17.0", ClusterTypeInteractive, 17, 0},
125+
{"17.1", ClusterTypeInteractive, 17, 1},
126+
{"17.2", ClusterTypeInteractive, 17, 2},
127+
{"17.3", ClusterTypeInteractive, 17, 3},
128+
{"15.4", ClusterTypeInteractive, 15, 4},
129+
}
130+
131+
for _, tt := range tests {
132+
t.Run(tt.version, func(t *testing.T) {
133+
v := ParseVersion(tt.version)
134+
assert.Equal(t, tt.expectedType, v.Type)
135+
assert.Equal(t, tt.expectedMajor, v.Major)
136+
assert.Equal(t, tt.expectedMinor, v.Minor)
137+
assert.Equal(t, tt.version, v.Raw)
138+
})
139+
}
140+
}
141+
142+
func TestParseVersion_Empty(t *testing.T) {
143+
v := ParseVersion("")
144+
assert.Equal(t, ClusterTypeUnknown, v.Type)
145+
assert.Equal(t, 0, v.Major)
146+
assert.Equal(t, 0, v.Minor)
147+
assert.Equal(t, "", v.Raw)
148+
}
149+
150+
func TestClusterType_String(t *testing.T) {
151+
assert.Equal(t, "interactive", ClusterTypeInteractive.String())
152+
assert.Equal(t, "serverless", ClusterTypeServerless.String())
153+
assert.Equal(t, "unknown", ClusterTypeUnknown.String())
154+
}
155+
156+
func TestContext_GetVersion(t *testing.T) {
157+
ctx := context.Background()
158+
159+
// Test serverless version
160+
serverlessCtx := MockRuntime(ctx, Environment{IsDbr: true, Version: "client.4.9"})
161+
v := GetVersion(serverlessCtx)
162+
assert.Equal(t, ClusterTypeServerless, v.Type)
163+
assert.Equal(t, 4, v.Major)
164+
assert.Equal(t, 9, v.Minor)
165+
166+
// Test interactive version
167+
interactiveCtx := MockRuntime(ctx, Environment{IsDbr: true, Version: "17.3"})
168+
v = GetVersion(interactiveCtx)
169+
assert.Equal(t, ClusterTypeInteractive, v.Type)
170+
assert.Equal(t, 17, v.Major)
171+
assert.Equal(t, 3, v.Minor)
172+
}

0 commit comments

Comments
 (0)