-
Notifications
You must be signed in to change notification settings - Fork 218
Improve AIR log retrieval reliability #6414
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ben-hansen-db
wants to merge
1
commit into
main
Choose a base branch
from
ben-hansen/air-log-retrieval-reliability
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Improved AIR log fallback, retry-attempt selection, and Unity Catalog volume artifact handling. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package aircmd | |
| import ( | ||
| "bufio" | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net" | ||
|
|
@@ -12,16 +13,47 @@ import ( | |
| "regexp" | ||
| "slices" | ||
| "strconv" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/databricks/cli/libs/filer" | ||
| "github.com/databricks/cli/libs/log" | ||
| "github.com/databricks/databricks-sdk-go" | ||
| "github.com/databricks/databricks-sdk-go/apierr" | ||
| "github.com/databricks/databricks-sdk-go/client" | ||
| "github.com/databricks/databricks-sdk-go/listing" | ||
| "github.com/databricks/databricks-sdk-go/service/jobs" | ||
| "github.com/databricks/databricks-sdk-go/service/ml" | ||
| ) | ||
|
|
||
| var mlflowArtifactRoots sync.Map | ||
|
|
||
| func mlflowArtifactRoot(ctx context.Context, w *databricks.WorkspaceClient, mlflowRunID string) (string, error) { | ||
| key := strings.TrimRight(w.Config.Host, "/") + "\x00" + mlflowRunID | ||
| if cached, ok := mlflowArtifactRoots.Load(key); ok { | ||
| return cached.(string), nil | ||
| } | ||
| response, err := w.Experiments.GetRun(ctx, ml.GetRunRequest{RunId: mlflowRunID}) | ||
| if err != nil { | ||
| log.Debugf(ctx, "air logs: could not resolve MLflow artifact URI for %s: %v", mlflowRunID, err) | ||
| return "", nil | ||
| } | ||
| if response.Run == nil || response.Run.Info == nil { | ||
| return "", nil | ||
| } | ||
| root := strings.TrimRight(response.Run.Info.ArtifactUri, "/") | ||
| mlflowArtifactRoots.Store(key, root) | ||
| return root, nil | ||
| } | ||
|
|
||
| func volumeArtifactRoot(root string) (string, bool) { | ||
| if strings.HasPrefix(root, "dbfs:/Volumes/") { | ||
| return strings.TrimPrefix(root, "dbfs:"), true | ||
| } | ||
| return "", false | ||
| } | ||
|
|
||
| // chunkFilePattern matches a log chunk file (logs-<index>.chunk.txt); group 1 is | ||
| // the chunk index. The sidecar splits stdout into 4MB chunks, index ascending. | ||
| var chunkFilePattern = regexp.MustCompile(`^logs-(\d+)\.chunk\.txt$`) | ||
|
|
@@ -53,6 +85,13 @@ func mlflowLogFallback(ctx context.Context, w *databricks.WorkspaceClient, out i | |
| log.Debugf(ctx, "air logs: --minutes is not supported on the MLflow fallback path; showing the default tail") | ||
| } | ||
|
|
||
| if !status.terminal() && !req.staticView { | ||
| return streamMLflowLogs(ctx, w, out, req, status) | ||
| } | ||
| return fetchMLflowLogTail(ctx, w, out, req, status) | ||
| } | ||
|
|
||
| func fetchMLflowLogTail(ctx context.Context, w *databricks.WorkspaceClient, out io.Writer, req logRequest, status logRunStatus) (bool, error) { | ||
| mlflowRunID, logDir, err := resolveMLflowLogPath(ctx, w, req) | ||
| if err != nil { | ||
| return false, err | ||
|
|
@@ -94,19 +133,113 @@ func mlflowLogFallback(ctx context.Context, w *databricks.WorkspaceClient, out i | |
| return status.downloadOutcome(), nil | ||
| } | ||
|
|
||
| func mlflowLogsExist(ctx context.Context, w *databricks.WorkspaceClient, req logRequest) bool { | ||
| mlflowRunID, logDir, err := resolveMLflowLogPath(ctx, w, req) | ||
| if err != nil || mlflowRunID == "" || logDir == "" { | ||
| if err != nil { | ||
| log.Debugf(ctx, "air logs: MLflow log-existence probe failed for run %d: %v", req.runID, err) | ||
| } | ||
| return false | ||
| } | ||
| chunks, err := listLogChunks(ctx, w, mlflowRunID, logDir) | ||
| if err != nil { | ||
| log.Debugf(ctx, "air logs: MLflow log-existence probe failed for run %d: %v", req.runID, err) | ||
| return false | ||
| } | ||
| return len(chunks) > 0 | ||
| } | ||
|
|
||
| func streamMLflowLogs(ctx context.Context, w *databricks.WorkspaceClient, out io.Writer, req logRequest, status logRunStatus) (bool, error) { | ||
| lineOffsets := make(map[string]int) | ||
| currentRunID := "" | ||
| printed := false | ||
| previousState := "" | ||
|
|
||
| for { | ||
| if req.onStatusChange != nil { | ||
| current := status.displayState() | ||
| if current != previousState { | ||
| req.onStatusChange(current, previousState) | ||
| previousState = current | ||
| } | ||
| } | ||
|
|
||
| mlflowRunID, logDir, err := resolveMLflowLogPath(ctx, w, req) | ||
| if err != nil { | ||
| if status.terminal() { | ||
| return false, err | ||
| } | ||
| log.Debugf(ctx, "air logs: failed to resolve MLflow log path: %v", err) | ||
| } else if mlflowRunID != "" && logDir != "" { | ||
| if currentRunID != mlflowRunID { | ||
| currentRunID = mlflowRunID | ||
| lineOffsets = make(map[string]int) | ||
| } | ||
| chunks, listErr := listLogChunks(ctx, w, mlflowRunID, logDir) | ||
| if listErr != nil { | ||
| if status.terminal() { | ||
| return false, listErr | ||
| } | ||
| log.Debugf(ctx, "air logs: failed to list MLflow log chunks: %v", listErr) | ||
| } else { | ||
| for _, chunk := range chunks { | ||
| lines, downloadErr := downloadChunkLines(ctx, w, mlflowRunID, chunk.path) | ||
| if downloadErr != nil { | ||
| if status.terminal() { | ||
| return false, downloadErr | ||
| } | ||
| log.Debugf(ctx, "air logs: failed to read MLflow log chunk %s: %v", chunk.path, downloadErr) | ||
| continue | ||
| } | ||
| offset := lineOffsets[chunk.path] | ||
| if offset > len(lines) { | ||
| offset = 0 | ||
| } | ||
| for _, line := range lines[offset:] { | ||
| emitLogLine(out, req, line) | ||
| printed = true | ||
| } | ||
| lineOffsets[chunk.path] = len(lines) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if status.terminal() { | ||
| if !printed { | ||
| emitNoLogs(out, req, status) | ||
| } | ||
| return status.succeeded(), nil | ||
| } | ||
| if err := sleepOrCancel(ctx, retryCheckInterval); err != nil { | ||
| return false, err | ||
| } | ||
| refreshed, err := resolveRunStatus(ctx, w, req.runID) | ||
| if err != nil { | ||
| if errors.Is(err, apierr.ErrResourceDoesNotExist) || ctx.Err() != nil { | ||
| return false, err | ||
| } | ||
| log.Debugf(ctx, "air logs: failed to refresh run status on MLflow fallback: %v", err) | ||
| continue | ||
| } | ||
| status = refreshed | ||
| } | ||
| } | ||
|
|
||
| // resolveMLflowLogPath returns the run's MLflow run id and per-node log directory. | ||
| func resolveMLflowLogPath(ctx context.Context, w *databricks.WorkspaceClient, req logRequest) (string, string, error) { | ||
| run, err := w.Jobs.GetRun(ctx, jobs.GetRunRequest{RunId: req.runID}) | ||
| if err != nil { | ||
| return "", "", err | ||
| } | ||
| ids := mlflowIDs(ctx, w, run) | ||
| attempt, taskRunID, err := resolveLogAttempt(run, req.attempt) | ||
| if err != nil { | ||
| return "", "", err | ||
| } | ||
| ids := mlflowIDsForTask(ctx, w, taskRunID) | ||
| if ids == nil || ids.RunID == "" { | ||
| return "", "", nil | ||
| } | ||
|
|
||
| // -1 (latest) maps to attempt 0's directory. | ||
| attempt := max(req.attempt, 0) | ||
| withAttempt, err := discoverAttemptPrefix(ctx, w, ids.RunID, attempt) | ||
| if err != nil { | ||
| return "", "", err | ||
|
|
@@ -224,8 +357,35 @@ func downloadChunkLines(ctx context.Context, w *databricks.WorkspaceClient, mlfl | |
| } | ||
|
|
||
| // listArtifacts lists a run's artifacts under a path. | ||
| func listArtifacts(ctx context.Context, w *databricks.WorkspaceClient, mlflowRunID, path string) ([]ml.FileInfo, error) { | ||
| it := w.Experiments.ListArtifacts(ctx, ml.ListArtifactsRequest{RunId: mlflowRunID, Path: path}) | ||
| func listArtifacts(ctx context.Context, w *databricks.WorkspaceClient, mlflowRunID, artifactPath string) ([]ml.FileInfo, error) { | ||
| root, err := mlflowArtifactRoot(ctx, w, mlflowRunID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if volumeRoot, ok := volumeArtifactRoot(root); ok { | ||
| fc, err := filer.NewWorkspaceFilesClient(w, volumeRoot) | ||
|
Comment on lines
+365
to
+366
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pls verify that |
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| entries, err := fc.ReadDir(ctx, artifactPath) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| files := make([]ml.FileInfo, 0, len(entries)) | ||
| for _, entry := range entries { | ||
| info := ml.FileInfo{Path: path.Join(artifactPath, entry.Name()), IsDir: entry.IsDir()} | ||
| if !entry.IsDir() { | ||
| stat, err := entry.Info() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| info.FileSize = stat.Size() | ||
| } | ||
| files = append(files, info) | ||
| } | ||
| return files, nil | ||
| } | ||
| it := w.Experiments.ListArtifacts(ctx, ml.ListArtifactsRequest{RunId: mlflowRunID, Path: artifactPath}) | ||
| return listing.ToSlice(ctx, it) | ||
| } | ||
|
|
||
|
|
@@ -247,6 +407,35 @@ type credentialsForReadResponse struct { | |
| // path. credentials-for-read returns a pre-signed URL, which we stream to disk; | ||
| // that endpoint is not modeled by the SDK, so it is called via a raw client.Do. | ||
| func downloadArtifact(ctx context.Context, w *databricks.WorkspaceClient, mlflowRunID, artifactPath string) (string, error) { | ||
| root, err := mlflowArtifactRoot(ctx, w, mlflowRunID) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| if volumeRoot, ok := volumeArtifactRoot(root); ok { | ||
| fc, err := filer.NewWorkspaceFilesClient(w, volumeRoot) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| reader, err := fc.Read(ctx, artifactPath) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| defer reader.Close() | ||
| tmp, err := os.CreateTemp("", "air-log-chunk-*") | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| if _, err := io.Copy(tmp, reader); err != nil { | ||
| tmp.Close() | ||
| os.Remove(tmp.Name()) | ||
| return "", err | ||
| } | ||
| if err := tmp.Close(); err != nil { | ||
| os.Remove(tmp.Name()) | ||
| return "", err | ||
| } | ||
| return tmp.Name(), nil | ||
| } | ||
| apiClient, err := client.New(w.Config) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to create API client: %w", err) | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see a world where we don't have the MLflow artifact fallback but I'm not sure when bricklens will be stable so we should include it