|
46 | 46 | )
|
47 | 47 | from pghoard.receivexlog import PGReceiveXLog
|
48 | 48 | from pghoard.transfer import TransferAgent, TransferQueue, UploadEvent
|
| 49 | +from pghoard.wal import WAL_SEG_SIZE |
49 | 50 | from pghoard.walreceiver import WALReceiver
|
50 | 51 | from pghoard.webserver import WebServer
|
51 | 52 |
|
@@ -934,8 +935,97 @@ def _get_all_threads(self):
|
934 | 935 | all_threads.extend(self.transfer_agents)
|
935 | 936 | return all_threads
|
936 | 937 |
|
| 938 | + def _rename_partial_files_if_completed(self, site: str, wal_location: str) -> None: |
| 939 | + """ |
| 940 | + Check for remaining partial WAL files generated by pg_receivewal, in case the partial file is completed, |
| 941 | + it will be renamed. |
| 942 | + """ |
| 943 | + # consider only the last partial file (pg_receivewal should only generate one) |
| 944 | + last_partial_file = None |
| 945 | + for partial_file in os.listdir(wal_location): |
| 946 | + if not partial_file.endswith(".partial"): |
| 947 | + continue |
| 948 | + |
| 949 | + if not last_partial_file or partial_file > last_partial_file: |
| 950 | + last_partial_file = partial_file |
| 951 | + |
| 952 | + if last_partial_file is None: |
| 953 | + return |
| 954 | + |
| 955 | + # check if the partial file needs to be renamed |
| 956 | + partial_file_path = os.path.join(wal_location, last_partial_file) |
| 957 | + renamed_partial_file_path = partial_file_path.replace(".partial", "") |
| 958 | + file_stats = os.stat(os.path.join(wal_location, last_partial_file)) |
| 959 | + |
| 960 | + if file_stats.st_size != WAL_SEG_SIZE: |
| 961 | + # TODO: handle partial files that are incompleted |
| 962 | + return |
| 963 | + |
| 964 | + # this will not trigger inotify |
| 965 | + os.rename(partial_file_path, renamed_partial_file_path) |
| 966 | + |
| 967 | + compression_event = CompressionEvent( |
| 968 | + file_type=FileType.Wal, |
| 969 | + file_path=FileTypePrefixes[FileType.Wal] / last_partial_file.replace(".partial", ""), |
| 970 | + delete_file_after_compression=True, |
| 971 | + backup_site_name=site, |
| 972 | + source_data=Path(renamed_partial_file_path), |
| 973 | + callback_queue=None, |
| 974 | + metadata={} |
| 975 | + ) |
| 976 | + self.compression_queue.put(compression_event) |
| 977 | + |
| 978 | + def _wait_for_queue_to_be_emptied( |
| 979 | + self, |
| 980 | + queue: Queue, |
| 981 | + queue_name: str, |
| 982 | + timeout: Optional[int] = None, |
| 983 | + ) -> None: |
| 984 | + start = time.monotonic() |
| 985 | + while True: |
| 986 | + if queue.empty(): |
| 987 | + self.log.info("%r queue has been emptied.", queue_name) |
| 988 | + break |
| 989 | + |
| 990 | + if timeout is not None and time.monotonic() - start > timeout: |
| 991 | + self.log.warning("Exceeded waiting time for %r queue to be emptied", queue_name) |
| 992 | + break |
| 993 | + |
| 994 | + time.sleep(0.1) |
| 995 | + |
937 | 996 | def handle_exit_signal(self, _signal=None, _frame=None): # pylint: disable=unused-argument
|
938 | 997 | self.log.warning("Quitting, signal: %r", _signal)
|
| 998 | + if _signal == signal.SIGTERM: |
| 999 | + self.graceful_shutdown() |
| 1000 | + else: |
| 1001 | + self.quit() |
| 1002 | + |
| 1003 | + def graceful_shutdown(self) -> None: |
| 1004 | + """ |
| 1005 | + Makes sure all missing files are compressed, uploaded and deleted. Also handles completed partial files |
| 1006 | + that might have not been renamed after shutting down receivexlogs. |
| 1007 | +
|
| 1008 | + Steps to follow: |
| 1009 | + - Shutdown receivexlogs and walreceivers threads |
| 1010 | + - Check for partial segments and rename them (if completed) |
| 1011 | + - Wait for compression, transfer and deletion queues to be empty |
| 1012 | + - Quit (stop remaining threads and write state file) |
| 1013 | + """ |
| 1014 | + self.log.info("Gracefully shutting down...") |
| 1015 | + self.running = False |
| 1016 | + for site, thread in {**self.receivexlogs, **self.walreceivers}.items(): |
| 1017 | + thread.running = False |
| 1018 | + |
| 1019 | + if not isinstance(thread, PGReceiveXLog): |
| 1020 | + continue |
| 1021 | + |
| 1022 | + self._rename_partial_files_if_completed(site=site, wal_location=thread.wal_location) |
| 1023 | + |
| 1024 | + # wait for all queues to be emptied |
| 1025 | + self._wait_for_queue_to_be_emptied(self.compression_queue, "compression") |
| 1026 | + self._wait_for_queue_to_be_emptied(self.transfer_queue, "transfer") |
| 1027 | + self._wait_for_queue_to_be_emptied(self.wal_file_deletion_queue, "wal_file_deletion") |
| 1028 | + |
939 | 1029 | self.quit()
|
940 | 1030 |
|
941 | 1031 | def quit(self):
|
|
0 commit comments