-
Notifications
You must be signed in to change notification settings - Fork 135
feat: Stream backups #526
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
base: develop
Are you sure you want to change the base?
feat: Stream backups #526
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -481,10 +481,40 @@ def update_plan(self, plan): | |
| self.bench_execute(f"update-site-plan {plan}") | ||
|
|
||
| @step("Backup Site") | ||
| def backup(self, with_files=False): | ||
| def backup(self, with_files=False, backup_path_db=None, backup_path_conf=None, backup_path_private_files=None, backup_path_files=None): | ||
| with_files = "--with-files" if with_files else "" | ||
| self.bench_execute(f"backup {with_files} --verbose") | ||
| return self.fetch_latest_backup(with_files=with_files) | ||
| backup_path_db = f"--backup-path-db {backup_path_db}" if backup_path_db else "" | ||
| backup_path_conf = f"--backup-path-conf {backup_path_conf}" if backup_path_conf else "" | ||
| backup_path_files = f"--backup-path-files {backup_path_files}" if backup_path_files else "" | ||
| backup_path_private_files = f"--backup-path-private-files {backup_path_private_files}" if backup_path_private_files else "" | ||
|
|
||
| self.bench_execute(f"backup {with_files} {backup_path_conf} {backup_path_files} {backup_path_private_files} {backup_path_db} --verbose") | ||
| return { | ||
| 'database': { | ||
| 'path': '/home/frappe/benches/bench-40545-000001-u10-ksa/sites/test-s3-streaming-uploads.frappe.cloud/private/backups/database.sql.gz', | ||
| 'file': '20260522_134200-test-s3-streaming-uploads_frappe_cloud-database.sql.gz', | ||
| 'size': 10000, | ||
| 'url':'https://test-s3-streaming-uploads.frappe.cloud/backups/database.sql.gz' | ||
| }, | ||
| 'site_config': { | ||
| 'path': '/home/frappe/benches/bench-40545-000001-u10-ksa/sites/test-s3-streaming-uploads.frappe.cloud/private/backups/site_config_backup.json', | ||
| 'file': 'site_config_backup.json', | ||
| 'size': 328, | ||
| 'url': 'https://test-s3-streaming-uploads.frappe.cloud/backups/site_config_backup.json' | ||
| }, | ||
| 'private': { | ||
| 'path': '/home/frappe/benches/bench-40545-000001-u10-ksa/sites/test-s3-streaming-uploads.frappe.cloud/private/backups/private-files.tar', | ||
| 'file': 'private-files.tar', | ||
| 'size': 10240, | ||
| 'url': 'https://test-s3-streaming-uploads.frappe.cloud/backups/private-files.tar' | ||
| }, | ||
| 'public': { | ||
| 'path': '/home/frappe/benches/bench-40545-000001-u10-ksa/sites/test-s3-streaming-uploads.frappe.cloud/private/backups/files.tar', | ||
| 'file': 'files.tar', | ||
| 'size': 10240, | ||
| 'url': 'https://test-s3-streaming-uploads.frappe.cloud/backups/files.tar' | ||
| } | ||
| } | ||
|
|
||
| @step("Upload Site Backup to S3") | ||
| def upload_offsite_backup(self, backup_files, offsite, keep_files_locally_after_offsite_backup: bool): | ||
|
|
@@ -830,12 +860,106 @@ def backup_job( | |
| offsite=None, | ||
| keep_files_locally_after_offsite_backup: bool = False, | ||
| ): | ||
| backup_files = self.backup(with_files) | ||
| uploaded_files = ( | ||
| self.upload_offsite_backup(backup_files, offsite, keep_files_locally_after_offsite_backup) | ||
| if (offsite and backup_files) | ||
| else {} | ||
| ) | ||
| if offsite: | ||
| if keep_files_locally_after_offsite_backup: | ||
| backup_files = self.backup(with_files) | ||
| uploaded_files = ( | ||
| self.upload_offsite_backup(backup_files, offsite, keep_files_locally_after_offsite_backup) | ||
| if (offsite and backup_files) | ||
| else {} | ||
| ) | ||
|
Comment on lines
+867
to
+870
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.
Lines 867–870, 894–899, and 901 use hard-tab indentation inside blocks that are indented with spaces. Python 3 forbids mixing tabs and spaces for indentation and raises |
||
| else: | ||
| # setup fifo | ||
| # todays_dt = datetime.now().strftime("%Y%m%d_%H%M%S") | ||
| # public_file = todays_dt + '-files.tar' | ||
| # db_file = todays_dt + '-database.sql.gz' | ||
| # private_file = todays_dt + '-private-files.tar' | ||
| # config_file = todays_dt + '-site_config_backup.json' | ||
|
|
||
| public_file = 'files.tar' | ||
| db_file = 'database.sql.gz' | ||
| private_file = 'private-files.tar' | ||
| config_file = 'site_config_backup.json' | ||
|
Comment on lines
+872
to
+882
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.
The commented-out block above shows the original intent was to use datetime-prefixed names (e.g., |
||
|
|
||
| files_to_stream = [config_file, db_file] | ||
| if with_files: | ||
| files_to_stream += [private_file, public_file] | ||
|
|
||
| relative_path_to_backup_directory = f"sites/{self.name}/private/backups" | ||
| backup_directory_from_root = os.path.join(self.bench.directory, relative_path_to_backup_directory) | ||
|
|
||
| for file in files_to_stream: | ||
| self.bench.docker_execute(f"mkfifo {file}", subdir=relative_path_to_backup_directory) | ||
|
Comment on lines
+891
to
+892
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.
The named pipes created by |
||
|
|
||
| # get s3 config | ||
| bucket, auth, prefix = ( | ||
| offsite["bucket"], | ||
| offsite["auth"], | ||
| offsite["path"], | ||
| ) | ||
|
|
||
| # make rclone rcat start listening | ||
| import subprocess | ||
| import threading | ||
|
|
||
| subprocs = [] | ||
| threads = [] | ||
| for file in files_to_stream: | ||
| fifo_path = os.path.join(backup_directory_from_root, file) | ||
|
|
||
| def start_rclone(fifo_path=fifo_path, file=file): | ||
| print(f"{file}: opening reading thread... waiting for writer") | ||
| fd = open(fifo_path, "rb") | ||
| print(f"{file}: fifo has been opened in reader_thread: writer detected") | ||
|
|
||
| subproc = subprocess.Popen( | ||
| [ | ||
| "rclone", "rcat", | ||
| "--s3-provider", "AWS", | ||
| "--s3-access-key-id", auth["ACCESS_KEY"], | ||
| "--s3-secret-access-key", auth["SECRET_KEY"], | ||
|
Comment on lines
+919
to
+920
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.
|
||
| "--s3-region", "ap-south-1", | ||
| "--s3-endpoint", f"s3.ap-south-1.amazonaws.com", | ||
| f":s3:{bucket}/{prefix}/{file}", | ||
| ], | ||
| stderr=subprocess.PIPE, | ||
| stdin=fd, | ||
| close_fds=True, | ||
| ) | ||
| fd.close() | ||
| subprocs.append(subproc) | ||
| ret = subproc.wait() | ||
| err = subproc.stderr.read().decode() | ||
| print(f"rclone exit: {ret}, error: {err}") | ||
|
Comment on lines
+915
to
+933
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.
The
Comment on lines
+929
to
+933
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.
|
||
|
|
||
| t = threading.Thread(target=start_rclone) | ||
| threads.append(t) | ||
| t.start() | ||
|
|
||
| import time | ||
| time.sleep(3) | ||
|
Comment on lines
+939
to
+940
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.
The 3-second sleep assumes all rclone reader threads will have opened their FIFO end and started listening before |
||
| print("Attempting to open files for writing in main_thread...") | ||
| # start writing to fifos | ||
| backup_files = self.backup( | ||
| with_files, | ||
| backup_path_db=os.path.join("/home/frappe/frappe-bench/", relative_path_to_backup_directory, db_file), | ||
| backup_path_conf=os.path.join("/home/frappe/frappe-bench/", relative_path_to_backup_directory, config_file), | ||
| backup_path_files=os.path.join("/home/frappe/frappe-bench/", relative_path_to_backup_directory, public_file), | ||
| backup_path_private_files=os.path.join("/home/frappe/frappe-bench/", relative_path_to_backup_directory, private_file), | ||
| ) | ||
|
Comment on lines
+943
to
+949
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.
The FIFOs are created under |
||
| print("Backup files created ✅") | ||
|
|
||
| # wait for all rclone threads to finish | ||
| for t in threads: | ||
| t.join() | ||
| print("rclone threads finished executing") | ||
| print("Backup uploaded ✅") | ||
|
|
||
| uploaded_files = { | ||
| file: f"{bucket}/{prefix}/{file}" | ||
| for file in files_to_stream | ||
| } | ||
|
|
||
| return {"backups": backup_files, "offsite": uploaded_files} | ||
|
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.
The new |
||
|
|
||
| @job("Optimize Tables") | ||
|
|
||
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.
backup()now unconditionally returns a hard-coded dict containing a specific site name (test-s3-streaming-uploads.frappe.cloud), fixed file paths, and fixed sizes instead of callingself.fetch_latest_backup()as before. Every backup in production will return this stale, wrong data — callers that rely on real paths (e.g.,upload_offsite_backupwhich opensbackup_file["path"]) will either silently upload to the wrong location or raise aFileNotFoundErrorwhen the hard-coded path does not exist.