¿Cómo compruebo que mi programa es resistente a los cierres inesperados?
Mi código python se ejecutará en un microcontrolador que se apaga inesperadamente. Me gustaría probar cada parte del código que se reinicia inesperadamente y verificar que lo maneje correctamente.
Intento: intenté poner el código en su propio proceso y luego lo terminé antes de tiempo, pero esto no funciona porque MyClass llama a 7zip desde la línea de comando, que continúa incluso después de que el proceso muere:
import multiprocessing import os def MyClass(multiprocessing.Process): ... def run(): os.system("7z a myfile.7z myfile") process = MyClass() process.start() time.sleep(4) print("terminating early") process.terminate() print("done")Lo que quiero:
class TestMyClass(unittest.TestCase): def test_MyClass_continuity(self): myclass = MyClass().start() myclass.kill_everything() myclass = MyClass().start() self.assert_everything_worked_as_expected()¿Hay una forma fácil de hacer esto? Si no, ¿cómo se diseña un código robusto que pueda terminar en cualquier punto (por ejemplo, probar máquinas de estado)?
Pregunta similar (sin respuesta al 26/10/21): simulación de terminación anormal en pytest
¡Muchas gracias!
Su lógica inicia un proceso envuelto dentro del objeto MyClass que a su vez genera un nuevo proceso a través de la llamada os.system .
Cuando finaliza el proceso MyClass , elimina el proceso principal pero deja el proceso 7zip ejecutándose como huérfano.
Además, el método process.terminate envía una señal SIGTERM al proceso secundario. El proceso hijo puede interceptar dicha señal y realizar algunas rutinas de limpieza antes de terminar. Esto no es ideal si desea simular una situación en la que no hay posibilidad de limpiar (una pérdida de energía). Lo más probable es que desee enviar una señal SIGKILL en su lugar (en Linux).
Para eliminar el proceso principal y el secundario, debe dirigirse a todo el grupo de procesos.
import os import time import signal import multiprocessing class MyClass(multiprocessing.Process): def run(self): # Ping localhost for a limited amount of time os.system("ping -c 12 127.0.0.1") process = MyClass() process.start() time.sleep(4) print("terminating early") # Send SIGKILL signal to the entire process group group_id = os.getpgid(process.pid) os.killpg(group_id, signal.SIGKILL) print("done")Lo anterior funciona solo en los sistemas operativos Unix y no en los de Windows.
Para Windows, debe usar el módulo psutil .
import os import time import multiprocessing import psutil class MyClass(multiprocessing.Process): def run(self): # Ping localhost for a limited amount of time os.system("ping -c 12 127.0.0.1") def kill_process_group(pid): process = psutil.Process(pid) children = process.children(recursive=True) # First terminate all children for child in children: child.kill() psutil.wait_procs(children) # Then terminate the parent process process.kill() process.wait() process = MyClass() process.start() time.sleep(4) print("terminating early") kill_process_group(process.pid) print("done")Creo que es una cuestión de persistencia y coherencia de los datos . Debe asegurarse de que todos los datos persistentes (es decir, escritos en el disco) también sean coherentes.
Imagine algún tipo de datos escritos en un archivo de estado. ¿Qué leerá la aplicación después de una terminación inesperada? ¿La mitad del nuevo estatus y la mitad del anterior? ¿La mitad del nuevo estado y el resto todo 0x00?
Entonces, la respuesta a su pregunta "¿Cómo se diseña un código robusto que pueda terminar en cualquier punto?" es usar operaciones atómicas cuando se trabaja con datos persistentes. La mayoría de las bases de datos dan algunas garantías en esa dirección. Y para trabajar con archivos locales yo personalmente suelo trabajar usando renombrar archivos . De esta manera, puedo escribir en un archivo temporal sin preocuparme por la consistencia y solo cuando haya terminado (¡asegúrese de vaciar los búferes!) de verdad y por lo tanto también persistente. Si en algún momento del proceso la aplicación finaliza inesperadamente, los datos persistentes siempre serán consistentes. Será el estado anterior (y algo de basura dentro de un archivo temporal) o el nuevo estado, pero nada intermedio.
Cualquiera que sea su elección, asegúrese de leer la documentación sobre la atomicidad para comprender lo que podría suceder. Es decir, un cambio de nombre de archivo interrumpido en el momento adecuado podría parecer la creación de un vínculo físico.
Tenga en cuenta que solo matar un proceso no es lo mismo que cortar la energía, porque el sistema operativo sigue ejecutándose y cierra archivos, vacía búferes, etc. Por ejemplo, cuando uso SQLite, rara vez veo "archivos de diario" cuando solo mato la aplicación, pero los veo muy a menudo al cortar la energía.