sourcegraph/dev/sg/msp/notion.go
Robert Lin 908d7119ea
chore/msp: blindly retry Notion page deletion (#63052)
Deleting Notion pages takes a very long time, and is prone to breaking in the page deletion step, where we must delete blocks one at a time because Notion does not allow for bulk block deletions. The errors seem to generally just be random Notion internal errors. This is very bad because it leaves go/msp-ops pages in an unusable state.

To try and mitigate, we add several places to blindly retry:

1. At the Notion SDK level, where a config option is available for retrying 429 errors
2. At the "reset page" helper level, where a failure to reset a page will prompt a retry of the whole helper
3. At the "delete blocks" helper level, where individual block deletion failures will be retried

Attempt to mitigate https://linear.app/sourcegraph/issue/CORE-119

While here, I also made some other QOL tweaks:

- Fix timing of sub-tasks in CLI output
- Bump default concurrency to 5 (our retries will handle if this is too aggressive, hopefully)
- Fix a missing space in generated docs

## Test plan

```
sg msp ops generate-handbook-pages   
```
2024-06-03 22:32:06 +00:00

135 lines
3.8 KiB
Go

package msp
import (
"context"
"github.com/jomei/notionapi"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
// resetNotionPage resets the given Notion page to the given title and removes
// all its children. For now, we reset the entire page and start from scratch
// each time. This will break Notion block links but it can't be helped, Notion
// is hard to work with.
func resetNotionPage(ctx context.Context, client *notionapi.Client, pageID, pageTitle string) error {
doReset := func() error {
blocks, err := listPageBlocks(ctx, client, pageID)
if err != nil {
return errors.Wrap(err, "failed to list page blocks")
}
if err := deleteBlocks(ctx, client, blocks); err != nil {
return errors.Wrap(err, "failed to delete blocks")
}
if err := setPageTitle(ctx, client, pageID, pageTitle); err != nil {
return errors.Wrap(err, "failed to set page title")
}
return nil
}
// Blindly retry 3 times, because Notion is very unreliable. We need to
// retry because leaving a page in a partially deleted state is not good.
const resetRetries = 3
var err error
for i := 0; i < resetRetries; i += 1 {
err = doReset()
if err == nil {
break
}
}
return err
}
func listPageBlocks(ctx context.Context, client *notionapi.Client, pageID string) (notionapi.Blocks, error) {
var blocks notionapi.Blocks
var cursor notionapi.Cursor
var pages int
for {
resp, err := client.Block.GetChildren(ctx, notionapi.BlockID(pageID), &notionapi.Pagination{
StartCursor: cursor,
PageSize: 100,
})
if err != nil {
return nil, errors.Wrapf(err, "page %d: failed to get children", pages)
}
for _, b := range resp.Results {
// Don't treat child pages as blocks on this page, they are different
// pages.
if b.GetType() != notionapi.BlockTypeChildPage {
blocks = append(blocks, b)
}
}
if !resp.HasMore {
break
}
pages += 1
cursor = notionapi.Cursor(resp.NextCursor)
}
return blocks, nil
}
func deleteBlocks(ctx context.Context, client *notionapi.Client, blocks notionapi.Blocks) error {
// WARNING: this cannot be paralellized, the Notion API will complain about
// a page-save-conflict. Ideally we can bulk-delete blocks, but that's not
// supported by Notion.
for _, block := range blocks {
// Blindly retry 3 times, because Notion is very unreliable.
const deleteRetryPerBlock = 3
var err error
for i := 0; i < deleteRetryPerBlock; i += 1 {
_, err = client.Block.Delete(ctx, block.GetID())
if err == nil {
break
}
}
if err != nil {
return errors.Wrapf(err, "delete block %q", block.GetID())
}
}
return nil
}
func setPageTitle(ctx context.Context, client *notionapi.Client, pageID string, title string) error {
if _, err := client.Page.Update(ctx,
notionapi.PageID(pageID),
&notionapi.PageUpdateRequest{
Properties: notionapi.Properties{
"title": notionapi.TitleProperty{
Type: notionapi.PropertyTypeTitle,
Title: []notionapi.RichText{{
Type: notionapi.ObjectTypeText,
Text: &notionapi.Text{Content: title},
}},
},
},
}); err != nil {
return errors.Wrap(err, "failed to set page title")
}
if _, err := client.Block.AppendChildren(ctx, notionapi.BlockID(pageID), &notionapi.AppendBlockChildrenRequest{
Children: []notionapi.Block{
notionapi.DividerBlock{
BasicBlock: notionapi.BasicBlock{
Object: notionapi.ObjectTypeBlock,
Type: notionapi.BlockTypeDivider,
},
},
notionapi.TableOfContentsBlock{
BasicBlock: notionapi.BasicBlock{
Object: notionapi.ObjectTypeBlock,
Type: notionapi.BlockTypeTableOfContents,
},
},
notionapi.DividerBlock{
BasicBlock: notionapi.BasicBlock{
Object: notionapi.ObjectTypeBlock,
Type: notionapi.BlockTypeDivider,
},
},
},
}); err != nil {
return errors.Wrap(err, "failed to add table of contents block")
}
return nil
}