|
1 | 1 | package dbos |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bytes" |
4 | 5 | "context" |
5 | 6 | "crypto/sha256" |
6 | 7 | "encoding/hex" |
7 | 8 | "fmt" |
| 9 | + "go/ast" |
| 10 | + "go/format" |
| 11 | + "go/parser" |
| 12 | + "go/token" |
8 | 13 | "log/slog" |
9 | 14 | "net/url" |
10 | 15 | "os" |
|
23 | 28 | _DEFAULT_ADMIN_SERVER_PORT = 3001 |
24 | 29 | ) |
25 | 30 |
|
26 | | -func computeApplicationVersion() string { |
27 | | - if len(registry) == 0 { |
28 | | - fmt.Println("DBOS: No registered workflows found, cannot compute application version") |
29 | | - return "" |
30 | | - } |
31 | | - |
32 | | - // Collect all function names and sort them for consistent hashing |
33 | | - var functionNames []string |
34 | | - for fqn := range registry { |
35 | | - functionNames = append(functionNames, fqn) |
36 | | - } |
37 | | - sort.Strings(functionNames) |
38 | | - |
39 | | - hasher := sha256.New() |
40 | | - |
41 | | - for _, fqn := range functionNames { |
42 | | - workflowEntry := registry[fqn] |
43 | | - |
44 | | - // Try to get function source location and other identifying info |
45 | | - if pc := runtime.FuncForPC(reflect.ValueOf(workflowEntry.wrappedFunction).Pointer()); pc != nil { |
46 | | - // Get the function's entry point - this reflects the actual compiled code |
47 | | - entry := pc.Entry() |
48 | | - fmt.Fprintf(hasher, "%x", entry) |
49 | | - } |
50 | | - } |
51 | | - |
52 | | - return hex.EncodeToString(hasher.Sum(nil)) |
53 | | - |
54 | | -} |
55 | | - |
56 | 31 | var workflowScheduler *cron.Cron // Global because accessed during workflow registration before the dbos singleton is initialized |
57 | 32 |
|
58 | 33 | var logger *slog.Logger // Global because accessed everywhere inside the library |
@@ -273,3 +248,90 @@ func Shutdown() { |
273 | 248 | } |
274 | 249 | dbos = nil |
275 | 250 | } |
| 251 | + |
| 252 | +func computeApplicationVersion() string { |
| 253 | + if len(registry) == 0 { |
| 254 | + fmt.Println("DBOS: No registered workflows found, cannot compute application version") |
| 255 | + return "" |
| 256 | + } |
| 257 | + |
| 258 | + // Create a file set for parsing Go source files |
| 259 | + fset := token.NewFileSet() |
| 260 | + |
| 261 | + // Collect function hashes instead of names for more precise versioning |
| 262 | + var functionHashes []string |
| 263 | + |
| 264 | + // Iterate through all registered workflow functions |
| 265 | + for fqn := range registry { |
| 266 | + workflowEntry := registry[fqn] |
| 267 | + |
| 268 | + // Get runtime program counter for the function to find its source location |
| 269 | + if pc := runtime.FuncForPC(reflect.ValueOf(workflowEntry.wrappedFunction).Pointer()); pc != nil { |
| 270 | + // Get the source file path and line number where function is defined |
| 271 | + file, line := pc.FileLine(pc.Entry()) |
| 272 | + |
| 273 | + // Parse the Go source file containing this function |
| 274 | + src, err := parser.ParseFile(fset, file, nil, parser.ParseComments) |
| 275 | + if err != nil { |
| 276 | + // If parsing fails, fallback to using function name as identifier |
| 277 | + getLogger().Warn("Failed to parse source file, using function name", "file", file, "function", fqn, "error", err) |
| 278 | + functionHashes = append(functionHashes, fqn) |
| 279 | + continue |
| 280 | + } |
| 281 | + |
| 282 | + // Extract the actual source code of the function |
| 283 | + funcSource := extractFunctionSource(src, fset, line) |
| 284 | + if funcSource != "" { |
| 285 | + // Hash the function's source code |
| 286 | + hasher := sha256.New() |
| 287 | + hasher.Write([]byte(funcSource)) |
| 288 | + functionHashes = append(functionHashes, hex.EncodeToString(hasher.Sum(nil))) |
| 289 | + } else { |
| 290 | + // If we can't find the function source, use the function name |
| 291 | + getLogger().Warn("Could not extract function source, using function name", "function", fqn, "line", line) |
| 292 | + functionHashes = append(functionHashes, fqn) |
| 293 | + } |
| 294 | + } else { |
| 295 | + // If we can't get runtime info, use the function name |
| 296 | + functionHashes = append(functionHashes, fqn) |
| 297 | + } |
| 298 | + } |
| 299 | + |
| 300 | + // Sort hashes for consistent ordering across runs |
| 301 | + sort.Strings(functionHashes) |
| 302 | + |
| 303 | + // Create final application version hash from all function hashes |
| 304 | + finalHasher := sha256.New() |
| 305 | + for _, hash := range functionHashes { |
| 306 | + fmt.Fprintf(finalHasher, "%s", hash) |
| 307 | + } |
| 308 | + |
| 309 | + return hex.EncodeToString(finalHasher.Sum(nil)) |
| 310 | +} |
| 311 | + |
| 312 | +// extractFunctionSource finds and returns the source code of a function at the given line |
| 313 | +func extractFunctionSource(file *ast.File, fset *token.FileSet, targetLine int) string { |
| 314 | + // Walk through all declarations in the file |
| 315 | + for _, decl := range file.Decls { |
| 316 | + // Check if this declaration is a function declaration |
| 317 | + if fn, ok := decl.(*ast.FuncDecl); ok { |
| 318 | + // Get the position information for this function |
| 319 | + startPos := fset.Position(fn.Pos()) |
| 320 | + endPos := fset.Position(fn.End()) |
| 321 | + |
| 322 | + // Check if the target line falls within this function's range |
| 323 | + if startPos.Line <= targetLine && targetLine <= endPos.Line { |
| 324 | + // Format the function AST back to source code |
| 325 | + var buf bytes.Buffer |
| 326 | + err := format.Node(&buf, fset, fn) |
| 327 | + if err != nil { |
| 328 | + getLogger().Warn("Failed to format function source", "function", fn.Name, "error", err) |
| 329 | + return "" |
| 330 | + } |
| 331 | + return buf.String() |
| 332 | + } |
| 333 | + } |
| 334 | + } |
| 335 | + // Function not found at the target line |
| 336 | + return "" |
| 337 | +} |
0 commit comments