github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/reporting_lists.go (about) 1 // Copyright 2023 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package main 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "net/http" 11 "sort" 12 "strings" 13 "time" 14 15 "github.com/google/syzkaller/dashboard/dashapi" 16 "github.com/google/syzkaller/pkg/hash" 17 "google.golang.org/appengine/v2" 18 db "google.golang.org/appengine/v2/datastore" 19 "google.golang.org/appengine/v2/log" 20 ) 21 22 // reportingPollBugLists is called by backends to get bug lists that need to be reported. 23 func reportingPollBugLists(c context.Context, typ string) []*dashapi.BugListReport { 24 state, err := loadReportingState(c) 25 if err != nil { 26 log.Errorf(c, "%v", err) 27 return nil 28 } 29 registry, err := makeSubsystemReportRegistry(c) 30 if err != nil { 31 log.Errorf(c, "%v", err) 32 return nil 33 } 34 ret := []*dashapi.BugListReport{} 35 for ns, nsConfig := range getConfig(c).Namespaces { 36 rConfig := nsConfig.Subsystems.Reminder 37 if rConfig == nil { 38 continue 39 } 40 reporting := nsConfig.ReportingByName(rConfig.SourceReporting) 41 stateEntry := state.getEntry(timeNow(c), ns, reporting.Name) 42 // The DB might well contain info about stale entities, but by querying the latest 43 // list of subsystems from the configuration, we make sure we only consider what's 44 // currently relevant. 45 rawSubsystems := nsConfig.Subsystems.Service.List() 46 // Sort to keep output stable. 47 sort.Slice(rawSubsystems, func(i, j int) bool { 48 return rawSubsystems[i].Name < rawSubsystems[j].Name 49 }) 50 for _, entry := range rawSubsystems { 51 if entry.NoReminders { 52 continue 53 } 54 for _, dbReport := range registry.get(ns, entry.Name) { 55 if stateEntry.Sent >= reporting.DailyLimit { 56 break 57 } 58 report, err := reportingBugListReport(c, dbReport, ns, entry.Name, typ) 59 if err != nil { 60 log.Errorf(c, "%v", err) 61 return nil 62 } 63 if report != nil { 64 ret = append(ret, report) 65 stateEntry.Sent++ 66 } 67 } 68 } 69 } 70 return ret 71 } 72 73 const maxNewListsPerNs = 5 74 75 // handleSubsystemReports is periodically invoked to construct fresh SubsystemReport objects. 76 func handleSubsystemReports(w http.ResponseWriter, r *http.Request) { 77 c := appengine.NewContext(r) 78 registry, err := makeSubsystemRegistry(c) 79 if err != nil { 80 log.Errorf(c, "failed to load subsystems: %v", err) 81 return 82 } 83 for ns, nsConfig := range getConfig(c).Namespaces { 84 rConfig := nsConfig.Subsystems.Reminder 85 if rConfig == nil { 86 continue 87 } 88 minPeriod := 24 * time.Hour * time.Duration(rConfig.PeriodDays) 89 reporting := nsConfig.ReportingByName(rConfig.SourceReporting) 90 var subsystems []*Subsystem 91 for _, entry := range nsConfig.Subsystems.Service.List() { 92 if entry.NoReminders { 93 continue 94 } 95 subsystems = append(subsystems, registry.get(ns, entry.Name)) 96 } 97 // Poll subsystems in a round-robin manner. 98 sort.Slice(subsystems, func(i, j int) bool { 99 return subsystems[i].ListsQueried.Before(subsystems[j].ListsQueried) 100 }) 101 updateLimit := maxNewListsPerNs 102 for _, subsystem := range subsystems { 103 if updateLimit == 0 { 104 break 105 } 106 if timeNow(c).Before(subsystem.LastBugList.Add(minPeriod)) { 107 continue 108 } 109 report, err := querySubsystemReport(c, subsystem, reporting, rConfig) 110 if err != nil { 111 log.Errorf(c, "failed to query bug lists: %v", err) 112 return 113 } 114 if err := registry.updatePoll(c, subsystem, report != nil); err != nil { 115 log.Errorf(c, "failed to update subsystem: %v", err) 116 return 117 } 118 if report == nil { 119 continue 120 } 121 updateLimit-- 122 if err := storeSubsystemReport(c, subsystem, report); err != nil { 123 log.Errorf(c, "failed to save subsystem: %v", err) 124 return 125 } 126 } 127 } 128 } 129 130 func reportingBugListCommand(c context.Context, cmd *dashapi.BugListUpdate) (string, error) { 131 // We have to execute it outside of the transacation, otherwise we get the 132 // "Only ancestor queries are allowed inside transactions." error. 133 subsystem, rawReport, _, err := findSubsystemReportByID(c, cmd.ID) 134 if err != nil { 135 return "", err 136 } 137 if subsystem == nil { 138 return "", fmt.Errorf("the bug list was not found") 139 } 140 reply := "" 141 tx := func(c context.Context) error { 142 subsystemKey := subsystemKey(c, subsystem) 143 reportKey := subsystemReportKey(c, subsystemKey, rawReport) 144 report := new(SubsystemReport) 145 if err := db.Get(c, reportKey, report); err != nil { 146 return fmt.Errorf("failed to query SubsystemReport (%v): %w", reportKey, err) 147 } 148 stage := report.findStage(cmd.ID) 149 if stage.ExtID == "" { 150 stage.ExtID = cmd.ExtID 151 } 152 if stage.Link == "" { 153 stage.Link = cmd.Link 154 } 155 // It might e.g. happen that we skipped a stage in reportingBugListReport. 156 // Make sure all skipped stages have non-nil Closed. 157 for i := range report.Stages { 158 item := &report.Stages[i] 159 if cmd.Command != dashapi.BugListRegenerateCmd && item == stage { 160 break 161 } 162 item.Closed = timeNow(c) 163 } 164 switch cmd.Command { 165 case dashapi.BugListSentCmd: 166 if !stage.Reported.IsZero() { 167 return fmt.Errorf("the reporting stage was already reported") 168 } 169 stage.Reported = timeNow(c) 170 171 state, err := loadReportingState(c) 172 if err != nil { 173 return fmt.Errorf("failed to query state: %w", err) 174 } 175 stateEnt := state.getEntry(timeNow(c), subsystem.Namespace, 176 getConfig(c).Namespaces[subsystem.Namespace].Subsystems.Reminder.SourceReporting) 177 stateEnt.Sent++ 178 if err := saveReportingState(c, state); err != nil { 179 return fmt.Errorf("failed to save state: %w", err) 180 } 181 case dashapi.BugListUpstreamCmd: 182 if !stage.Moderation { 183 reply = `The report cannot be sent further upstream. 184 It's already at the last reporting stage.` 185 return nil 186 } 187 if !stage.Closed.IsZero() { 188 reply = `The bug list was already upstreamed. 189 Please visit the new discussion thread.` 190 return nil 191 } 192 stage.Closed = timeNow(c) 193 case dashapi.BugListRegenerateCmd: 194 dbSubsystem := new(Subsystem) 195 err := db.Get(c, subsystemKey, dbSubsystem) 196 if err != nil { 197 return fmt.Errorf("failed to get subsystem: %w", err) 198 } 199 dbSubsystem.LastBugList = time.Time{} 200 _, err = db.Put(c, subsystemKey, dbSubsystem) 201 if err != nil { 202 return fmt.Errorf("failed to save subsystem: %w", err) 203 } 204 } 205 _, err = db.Put(c, reportKey, report) 206 if err != nil { 207 return fmt.Errorf("failed to save the object: %w", err) 208 } 209 return nil 210 } 211 return reply, db.RunInTransaction(c, tx, &db.TransactionOptions{ 212 XG: true, 213 Attempts: 10, 214 }) 215 } 216 217 func findSubsystemReportByID(c context.Context, ID string) (*Subsystem, 218 *SubsystemReport, *SubsystemReportStage, error) { 219 var subsystemReports []*SubsystemReport 220 reportKeys, err := db.NewQuery("SubsystemReport"). 221 Filter("Stages.ID=", ID). 222 Limit(1). 223 GetAll(c, &subsystemReports) 224 if err != nil { 225 return nil, nil, nil, fmt.Errorf("failed to query subsystem reports: %w", err) 226 } 227 if len(subsystemReports) == 0 { 228 return nil, nil, nil, nil 229 } 230 stage := subsystemReports[0].findStage(ID) 231 if stage == nil { 232 // This should never happen (provided that all the code is correct). 233 return nil, nil, nil, fmt.Errorf("bug list is found, but the stage is missing") 234 } 235 subsystem := new(Subsystem) 236 if err := db.Get(c, reportKeys[0].Parent(), subsystem); err != nil { 237 return nil, nil, nil, fmt.Errorf("failed to query subsystem: %w", err) 238 } 239 return subsystem, subsystemReports[0], stage, nil 240 } 241 242 // querySubsystemReport queries the open bugs and constructs a new SubsystemReport object. 243 func querySubsystemReport(c context.Context, subsystem *Subsystem, reporting *Reporting, 244 config *BugListReportingConfig) (*SubsystemReport, error) { 245 rawOpenBugs, fixedBugs, err := queryMatchingBugs(c, subsystem.Namespace, 246 subsystem.Name, reporting) 247 if err != nil { 248 return nil, err 249 } 250 withRepro, noRepro := []*Bug{}, []*Bug{} 251 for _, bug := range rawOpenBugs { 252 const possiblyFixedTimespan = 24 * time.Hour * 14 253 if bug.LastTime.Before(timeNow(c).Add(-possiblyFixedTimespan)) { 254 // The bug didn't happen recently, possibly it was already fixed. 255 // Let's not display such bugs in reminders. 256 continue 257 } 258 if bug.FirstTime.After(timeNow(c).Add(-config.MinBugAge)) { 259 // Don't take bugs which are too new -- they're still fresh in memory. 260 continue 261 } 262 if bug.prio() == LowPrioBug { 263 // Don't include low priority bugs in reports because the community 264 // actually perceives them as non-actionable. 265 continue 266 } 267 discussions := bug.discussionSummary() 268 if discussions.ExternalMessages > 0 && 269 discussions.LastMessage.After(timeNow(c).Add(-config.UserReplyFrist)) { 270 // Don't take bugs with recent user replies. 271 // As we don't keep exactly the date of the last user message, approximate it. 272 continue 273 } 274 if bug.HasLabel(NoRemindersLabel, "") { 275 // The bug was intentionally excluded from monthly reminders. 276 continue 277 } 278 if bug.ReproLevel == dashapi.ReproLevelNone { 279 noRepro = append(noRepro, bug) 280 } else { 281 withRepro = append(withRepro, bug) 282 } 283 } 284 // Let's reduce noise and don't remind about just one bug. 285 if len(noRepro)+len(withRepro) < config.MinBugsCount { 286 return nil, nil 287 } 288 // Even if we have enough bugs with a reproducer, there might still be bugs 289 // without a reproducer that have a lot of crashes. So let's take a small number 290 // of such bugs and give them a chance to be present in the final list. 291 takeNoRepro := 2 292 if takeNoRepro+len(withRepro) < config.BugsInReport { 293 takeNoRepro = config.BugsInReport - len(withRepro) 294 } 295 if takeNoRepro > len(noRepro) { 296 takeNoRepro = len(noRepro) 297 } 298 sort.Slice(noRepro, func(i, j int) bool { 299 return noRepro[i].NumCrashes > noRepro[j].NumCrashes 300 }) 301 takeBugs := append(withRepro, noRepro[:takeNoRepro]...) 302 sort.Slice(takeBugs, func(i, j int) bool { 303 firstPrio, secondPrio := takeBugs[i].prio(), takeBugs[j].prio() 304 if firstPrio != secondPrio { 305 return !firstPrio.LessThan(secondPrio) 306 } 307 if takeBugs[i].NumCrashes != takeBugs[j].NumCrashes { 308 return takeBugs[i].NumCrashes > takeBugs[j].NumCrashes 309 } 310 return takeBugs[i].Title < takeBugs[j].Title 311 }) 312 keys := []*db.Key{} 313 for _, bug := range takeBugs { 314 keys = append(keys, bug.key(c)) 315 } 316 if len(keys) > config.BugsInReport { 317 keys = keys[:config.BugsInReport] 318 } 319 report := makeSubsystemReport(c, config, keys) 320 report.TotalStats = makeSubsystemReportStats(c, rawOpenBugs, fixedBugs, 0) 321 report.PeriodStats = makeSubsystemReportStats(c, rawOpenBugs, fixedBugs, config.PeriodDays) 322 return report, nil 323 } 324 325 func makeSubsystemReportStats(c context.Context, open, fixed []*Bug, days int) SubsystemReportStats { 326 after := timeNow(c).Add(-time.Hour * 24 * time.Duration(days)) 327 ret := SubsystemReportStats{} 328 for _, bug := range open { 329 if days > 0 && bug.FirstTime.Before(after) { 330 continue 331 } 332 if bug.prio() == LowPrioBug { 333 ret.LowPrio++ 334 } else { 335 ret.Reported++ 336 } 337 } 338 for _, bug := range fixed { 339 if len(bug.CommitInfo) == 0 { 340 continue 341 } 342 if days > 0 && bug.CommitInfo[0].Date.Before(after) { 343 continue 344 } 345 ret.Fixed++ 346 } 347 return ret 348 } 349 350 func queryMatchingBugs(c context.Context, ns, name string, reporting *Reporting) ([]*Bug, []*Bug, error) { 351 allOpenBugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { 352 return query.Filter("Namespace=", ns). 353 Filter("Status=", BugStatusOpen). 354 Filter("Labels.Label=", SubsystemLabel). 355 Filter("Labels.Value=", name) 356 }) 357 if err != nil { 358 return nil, nil, fmt.Errorf("failed to query open bugs for subsystem: %w", err) 359 } 360 allFixedBugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { 361 return query.Filter("Namespace=", ns). 362 Filter("Status=", BugStatusFixed). 363 Filter("Labels.Label=", SubsystemLabel). 364 Filter("Labels.Value=", name) 365 }) 366 if err != nil { 367 return nil, nil, fmt.Errorf("failed to query fixed bugs for subsystem: %w", err) 368 } 369 open, fixed := []*Bug{}, []*Bug{} 370 for _, bug := range append(allOpenBugs, allFixedBugs...) { 371 if len(bug.Commits) != 0 || bug.Status == BugStatusFixed { 372 // This bug is no longer really open. 373 fixed = append(fixed, bug) 374 continue 375 } 376 currReporting, _, _, _, err := currentReporting(c, bug) 377 if err != nil { 378 continue 379 } 380 if reporting.Name != currReporting.Name { 381 // The bug is not at the expected reporting stage. 382 continue 383 } 384 if currReporting.AccessLevel > reporting.AccessLevel { 385 continue 386 } 387 open = append(open, bug) 388 } 389 return open, fixed, nil 390 } 391 392 // makeSubsystemReport creates a new SubsystemReminder object. 393 func makeSubsystemReport(c context.Context, config *BugListReportingConfig, 394 keys []*db.Key) *SubsystemReport { 395 ret := &SubsystemReport{ 396 Created: timeNow(c), 397 } 398 for _, key := range keys { 399 ret.BugKeys = append(ret.BugKeys, key.Encode()) 400 } 401 baseID := hash.String([]byte(fmt.Sprintf("%v-%v", timeNow(c), ret.BugKeys))) 402 if config.ModerationConfig != nil { 403 ret.Stages = append(ret.Stages, SubsystemReportStage{ 404 ID: bugListReportingHash(baseID, "moderation"), 405 Moderation: true, 406 }) 407 } 408 ret.Stages = append(ret.Stages, SubsystemReportStage{ 409 ID: bugListReportingHash(baseID, "public"), 410 }) 411 return ret 412 } 413 414 const bugListHashPrefix = "list" 415 416 func bugListReportingHash(base, name string) string { 417 return bugListHashPrefix + bugReportingHash(base, name) 418 } 419 420 func isBugListHash(hash string) bool { 421 return strings.HasPrefix(hash, bugListHashPrefix) 422 } 423 424 func reportingBugListReport(c context.Context, subsystemReport *SubsystemReport, 425 ns, name, targetReportingType string) (*dashapi.BugListReport, error) { 426 for _, stage := range subsystemReport.Stages { 427 if !stage.Closed.IsZero() { 428 continue 429 } 430 repConfig := bugListReportingConfig(c, ns, &stage) 431 if repConfig == nil { 432 // It might happen if e.g. Moderation was set to nil. 433 // Just skip the stage then. 434 continue 435 } 436 if !stage.Reported.IsZero() || repConfig.Type() != targetReportingType { 437 break 438 } 439 configJSON, err := json.Marshal(repConfig) 440 if err != nil { 441 return nil, err 442 } 443 ret := &dashapi.BugListReport{ 444 ID: stage.ID, 445 Created: subsystemReport.Created, 446 Config: configJSON, 447 Link: fmt.Sprintf("%v/%s/s/%s", appURL(c), ns, name), 448 Subsystem: name, 449 Maintainers: subsystemMaintainers(c, ns, name), 450 Moderation: stage.Moderation, 451 TotalStats: subsystemReport.TotalStats.toDashapi(), 452 PeriodStats: subsystemReport.PeriodStats.toDashapi(), 453 PeriodDays: getNsConfig(c, ns).Subsystems.Reminder.PeriodDays, 454 } 455 bugKeys, err := subsystemReport.getBugKeys() 456 if err != nil { 457 return nil, fmt.Errorf("failed to get bug keys: %w", err) 458 } 459 bugs := make([]*Bug, len(bugKeys)) 460 err = db.GetMulti(c, bugKeys, bugs) 461 if err != nil { 462 return nil, fmt.Errorf("failed to get bugs: %w", err) 463 } 464 for _, bug := range bugs { 465 bugReporting := bugReportingByName(bug, 466 getNsConfig(c, ns).Subsystems.Reminder.SourceReporting) 467 ret.Bugs = append(ret.Bugs, dashapi.BugListItem{ 468 Title: bug.displayTitle(), 469 Link: fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID), 470 ReproLevel: bug.ReproLevel, 471 Hits: bug.NumCrashes, 472 }) 473 } 474 return ret, nil 475 } 476 return nil, nil 477 } 478 479 func bugListReportingConfig(c context.Context, ns string, stage *SubsystemReportStage) ReportingType { 480 cfg := getNsConfig(c, ns).Subsystems.Reminder 481 if stage.Moderation { 482 return cfg.ModerationConfig 483 } 484 return cfg.Config 485 } 486 487 func makeSubsystem(ns, name string) *Subsystem { 488 return &Subsystem{ 489 Namespace: ns, 490 Name: name, 491 } 492 } 493 494 func subsystemKey(c context.Context, s *Subsystem) *db.Key { 495 return db.NewKey(c, "Subsystem", fmt.Sprintf("%v-%v", s.Namespace, s.Name), 0, nil) 496 } 497 498 func subsystemReportKey(c context.Context, subsystemKey *db.Key, r *SubsystemReport) *db.Key { 499 return db.NewKey(c, "SubsystemReport", r.Created.UTC().Format(time.RFC822), 0, subsystemKey) 500 } 501 502 type subsystemsRegistry struct { 503 entities map[string]map[string]*Subsystem 504 } 505 506 func makeSubsystemRegistry(c context.Context) (*subsystemsRegistry, error) { 507 var subsystems []*Subsystem 508 if _, err := db.NewQuery("Subsystem").GetAll(c, &subsystems); err != nil { 509 return nil, err 510 } 511 ret := &subsystemsRegistry{ 512 entities: map[string]map[string]*Subsystem{}, 513 } 514 for _, item := range subsystems { 515 ret.store(item) 516 } 517 return ret, nil 518 } 519 520 func (sr *subsystemsRegistry) get(ns, name string) *Subsystem { 521 ret := sr.entities[ns][name] 522 if ret == nil { 523 ret = makeSubsystem(ns, name) 524 } 525 return ret 526 } 527 528 func (sr *subsystemsRegistry) store(item *Subsystem) { 529 if sr.entities[item.Namespace] == nil { 530 sr.entities[item.Namespace] = map[string]*Subsystem{} 531 } 532 sr.entities[item.Namespace][item.Name] = item 533 } 534 535 func (sr *subsystemsRegistry) updatePoll(c context.Context, s *Subsystem, success bool) error { 536 key := subsystemKey(c, s) 537 return db.RunInTransaction(c, func(c context.Context) error { 538 dbSubsystem := new(Subsystem) 539 err := db.Get(c, key, dbSubsystem) 540 if err == db.ErrNoSuchEntity { 541 dbSubsystem = s 542 } else if err != nil { 543 return fmt.Errorf("failed to get Subsystem '%v': %w", key, err) 544 } 545 dbSubsystem.ListsQueried = timeNow(c) 546 if success { 547 dbSubsystem.LastBugList = timeNow(c) 548 } 549 if _, err := db.Put(c, key, dbSubsystem); err != nil { 550 return fmt.Errorf("failed to save Subsystem: %w", err) 551 } 552 sr.store(dbSubsystem) 553 return nil 554 }, nil) 555 } 556 557 type subsystemReportRegistry struct { 558 entities map[string]map[string][]*SubsystemReport 559 } 560 561 func makeSubsystemReportRegistry(c context.Context) (*subsystemReportRegistry, error) { 562 var reports []*SubsystemReport 563 reportKeys, err := db.NewQuery("SubsystemReport").GetAll(c, &reports) 564 if err != nil { 565 return nil, err 566 } 567 var subsystemKeys []*db.Key 568 for _, key := range reportKeys { 569 subsystemKeys = append(subsystemKeys, key.Parent()) 570 } 571 subsystems := make([]*Subsystem, len(subsystemKeys)) 572 if err := db.GetMulti(c, subsystemKeys, subsystems); err != nil { 573 return nil, fmt.Errorf("failed to query subsystems: %w", err) 574 } 575 ret := &subsystemReportRegistry{ 576 entities: map[string]map[string][]*SubsystemReport{}, 577 } 578 for i, item := range reports { 579 ret.store(subsystems[i].Namespace, subsystems[i].Name, item) 580 } 581 return ret, nil 582 } 583 584 func (srr *subsystemReportRegistry) get(ns, name string) []*SubsystemReport { 585 return srr.entities[ns][name] 586 } 587 588 func (srr *subsystemReportRegistry) store(ns, name string, item *SubsystemReport) { 589 if srr.entities[ns] == nil { 590 srr.entities[ns] = map[string][]*SubsystemReport{} 591 } 592 srr.entities[ns][name] = append(srr.entities[ns][name], item) 593 } 594 595 func storeSubsystemReport(c context.Context, s *Subsystem, report *SubsystemReport) error { 596 key := subsystemKey(c, s) 597 return db.RunInTransaction(c, func(c context.Context) error { 598 // First close all previouly active per-subsystem reports. 599 var previous []*SubsystemReport 600 prevKeys, err := db.NewQuery("SubsystemReport"). 601 Ancestor(key). 602 Filter("Stages.Closed=", time.Time{}). 603 GetAll(c, &previous) 604 if err != nil { 605 return fmt.Errorf("failed to query old subsystem reports: %w", err) 606 } 607 for i, subsystem := range previous { 608 for i := range subsystem.Stages { 609 subsystem.Stages[i].Closed = timeNow(c) 610 } 611 if _, err := db.Put(c, prevKeys[i], subsystem); err != nil { 612 return fmt.Errorf("failed to save SubsystemReport: %w", err) 613 } 614 } 615 // Now save a new one. 616 reportKey := subsystemReportKey(c, key, report) 617 if _, err := db.Put(c, reportKey, report); err != nil { 618 return fmt.Errorf("failed to store new SubsystemReport: %w", err) 619 } 620 return nil 621 }, nil) 622 }