-
Notifications
You must be signed in to change notification settings - Fork 33
Feat/s3 large comments #5145
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
lsabor
wants to merge
16
commits into
main
Choose a base branch
from
feat/s3-large-comments
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
Feat/s3 large comments #5145
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
fc1bf35
feat: archive long private bot comment texts to S3
lsabor 0c7fedb
fix: truncate the base text column when archiving bot comments
lsabor 93a5268
adds syncing command
lsabor 7af60ec
test: cover syncing comment texts against an existing archive
lsabor fbb971b
fix: address review of the comment text archive
lsabor 25ec7e0
refactor: simplify the comment text archive commands
lsabor 16cdccc
Merge branch 'main' of github.com:Metaculus/metaculus into feat/s3-la…
lsabor 892e517
remove unused syn_Archived_comment_texts.py
lsabor 55d2eae
address comments
lsabor 8614b02
formatting
lsabor cbe573b
fix heavy call
lsabor beec3a8
add note to text at serialization time pointing at full retrieval loc…
lsabor 86f3288
cleanup, simplify and remove unnecessary tests
lsabor 456f6b7
revove useless comments
lsabor e9c576a
add frontend vs api discrimination in adding truncation text, pivot p…
lsabor 2c36b19
translations
lsabor 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
180 changes: 180 additions & 0 deletions
180
comments/management/commands/archive_bot_comment_texts.py
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,180 @@ | ||
| import time | ||
| from collections.abc import Callable | ||
|
|
||
| from django.conf import settings | ||
| from django.core.management.base import BaseCommand, CommandError | ||
|
|
||
| from comments.services.text_archive import ( | ||
| ARCHIVE_AGE_DAYS, | ||
| ARCHIVE_MIN_TEXT_LENGTH, | ||
| ARCHIVE_STUB_LENGTH, | ||
| DEFAULT_BATCH_SIZE, | ||
| DEFAULT_CONCURRENCY, | ||
| S3_KEY_PREFIX, | ||
| ArchiveStats, | ||
| archive_bot_comment_texts, | ||
| check_is_enabled, | ||
| ) | ||
|
|
||
|
|
||
| def format_duration(seconds: float) -> str: | ||
| seconds = int(seconds) | ||
| hours, remainder = divmod(seconds, 3600) | ||
| minutes, seconds = divmod(remainder, 60) | ||
|
|
||
| if hours: | ||
| return f"{hours}h{minutes:02d}m" | ||
| if minutes: | ||
| return f"{minutes}m{seconds:02d}s" | ||
|
|
||
| return f"{seconds}s" | ||
|
|
||
|
|
||
| class ProgressWriter: | ||
| """ | ||
| Prints a running one-line summary with a rate and an ETA. | ||
|
|
||
| The daily run has only a day of new comments to clear, but the first | ||
| backfill works through hundreds of thousands of rows over hours, so the | ||
| point is to make a long run observable rather than to look pretty. Output | ||
| is one line per batch, not a redrawn line, so it survives being piped to a | ||
| log file. | ||
| """ | ||
|
|
||
| def __init__(self, stdout, total: int = 0): | ||
| self.stdout = stdout | ||
| self.total = total | ||
| self.started = time.monotonic() | ||
|
|
||
| @property | ||
| def elapsed(self) -> float: | ||
| return time.monotonic() - self.started | ||
|
|
||
| def write(self, line: str) -> None: | ||
| self.stdout.write(line) | ||
| self.stdout.flush() | ||
|
|
||
| def update(self, done: int, summary: str, detail: str = "") -> None: | ||
| elapsed = self.elapsed | ||
| rate = done / elapsed if elapsed else 0 | ||
| percent = (done / self.total * 100) if self.total else 0 | ||
| remaining = max(self.total - done, 0) | ||
| eta = format_duration(remaining / rate) if rate else "?" | ||
|
|
||
| line = ( | ||
| f" {done:,}/{self.total:,} ({percent:.1f}%) {summary} " | ||
| f"{rate:.1f}/s elapsed {format_duration(elapsed)} eta {eta}" | ||
| ) | ||
|
|
||
| if detail: | ||
| line += f" [{detail}]" | ||
|
|
||
| self.write(line) | ||
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| help = ( | ||
| "Moves the full text of private bot comments older than " | ||
| f"{ARCHIVE_AGE_DAYS} days and longer than {ARCHIVE_MIN_TEXT_LENGTH} " | ||
| f"characters to S3, leaving a {ARCHIVE_STUB_LENGTH}-character stub in " | ||
| "the database. Runs daily as a cron job; the full text stays " | ||
| "readable through the comment-detail endpoint." | ||
| ) | ||
|
|
||
| def add_arguments(self, parser): | ||
| parser.add_argument( | ||
| "--dry-run", | ||
| action="store_true", | ||
| help="Report what would be archived without writing to S3 or the database", | ||
| ) | ||
| parser.add_argument( | ||
| "--limit", | ||
| type=int, | ||
| default=None, | ||
| help="Maximum number of comments to archive (useful for the first backfill)", | ||
| ) | ||
| parser.add_argument( | ||
| "--batch-size", | ||
| type=int, | ||
| default=DEFAULT_BATCH_SIZE, | ||
| help=f"Comments per database update (default: {DEFAULT_BATCH_SIZE})", | ||
| ) | ||
| parser.add_argument( | ||
| "--concurrency", | ||
| type=int, | ||
| default=DEFAULT_CONCURRENCY, | ||
| help=( | ||
| "Uploads to keep in flight at once. S3 has no multi-object PUT, " | ||
| "so this is what makes a large backfill finish in minutes " | ||
| f"rather than hours (default: {DEFAULT_CONCURRENCY})" | ||
| ), | ||
| ) | ||
|
|
||
| def handle(self, *args, **options): | ||
| dry_run = options["dry_run"] | ||
|
|
||
| if not check_is_enabled(): | ||
| raise CommandError( | ||
| "AWS_STORAGE_BUCKET_COMMENTS_TEXT is not configured, " | ||
| "comment text archiving is disabled." | ||
| ) | ||
|
|
||
| progress = ProgressWriter(self.stdout) | ||
| on_progress: Callable[[ArchiveStats], None] | None = None | ||
|
|
||
| if not dry_run: | ||
| progress.write( | ||
| f"Archiving to {settings.AWS_STORAGE_BUCKET_COMMENTS_TEXT}/" | ||
| f"{S3_KEY_PREFIX}/ with concurrency {options['concurrency']}, " | ||
| f"batches of {options['batch_size']}" | ||
| ) | ||
| progress.write("Finding eligible comments...") | ||
|
|
||
| def write_progress(stats: ArchiveStats) -> None: | ||
| progress.total = stats.total | ||
| detail = ", ".join( | ||
| f"{count} {label}" | ||
| for label, count in ( | ||
| ("failed", stats.failed), | ||
| ("skipped", stats.skipped), | ||
| ) | ||
| if count | ||
| ) | ||
| progress.update( | ||
| stats.archived + stats.failed + stats.skipped, | ||
| f"{stats.chars_reclaimed:,} chars reclaimed", | ||
| detail, | ||
| ) | ||
|
|
||
| on_progress = write_progress | ||
|
|
||
| stats = archive_bot_comment_texts( | ||
| dry_run=dry_run, | ||
| limit=options["limit"], | ||
| batch_size=options["batch_size"], | ||
| concurrency=options["concurrency"], | ||
| on_progress=on_progress, | ||
| ) | ||
|
|
||
| verb = "Would archive" if dry_run else "Archived" | ||
| elapsed = "" if dry_run else f" in {format_duration(progress.elapsed)}" | ||
| progress.write( | ||
| f"{verb} {stats.archived:,} comment(s), " | ||
| f"reclaiming {stats.chars_reclaimed:,} characters{elapsed}" | ||
| ) | ||
|
|
||
| if stats.sample_ids: | ||
| sample = ", ".join(str(pk) for pk in stats.sample_ids) | ||
| progress.write(f"Sample comment ids: {sample}") | ||
|
|
||
| if stats.skipped: | ||
| self.stdout.write( | ||
| self.style.WARNING( | ||
| f"Skipped {stats.skipped} comment(s) edited during the run" | ||
| ) | ||
| ) | ||
|
|
||
| if stats.failed: | ||
| self.stdout.write( | ||
| self.style.ERROR(f"Failed to upload {stats.failed} comment(s)") | ||
| ) |
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,18 @@ | ||
| # Generated by Django 5.2.15 on 2026-08-19 17:19 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('comments', '0026_comment_key_factor_votes_score'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='comment', | ||
| name='is_text_archived', | ||
| field=models.BooleanField(db_index=True, default=False, editable=False, help_text='True if the full text has been moved to S3 and only a truncated stub remains in the text columns. Archived comments cannot be edited; use the comment-detail endpoint to read them.'), | ||
| ), | ||
| ] |
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
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.
Uh oh!
There was an error while loading. Please reload this page.