Guía de transacciones

Introducción

Cuando se usa una biblioteca de C/C++ no aislada en un espacio aislado, el vinculador garantiza que todas las funciones necesarias estén disponibles después de la compilación, por lo que no hay necesidad de preocuparse por si una llamada a la API puede fallar en el tiempo de ejecución.

Sin embargo, cuando se usa una biblioteca con zona de pruebas, la ejecución de la biblioteca se realiza en un proceso separado. Si falla una llamada a la API, se deben verificar todos los tipos de problemas relacionados con el paso de la llamada a través de la capa de RPC. A veces, los errores de la capa de RPC pueden no ser de interés, por ejemplo, cuando se realiza un procesamiento masivo y se acaba de reiniciar la zona de pruebas.

Sin embargo, por los motivos mencionados anteriormente, es importante extender la verificación de errores habitual del valor de devolución de la llamada a la API en zona de pruebas para incluir la verificación de si se devolvió un error en la capa de RPC. Por eso, todos los prototipos de funciones de la biblioteca devuelven ::sapi::StatusOr<T> en lugar de T. En caso de que falle la invocación de la función de biblioteca (p.ej., debido a un incumplimiento de la zona de pruebas), el valor de devolución contendrá detalles sobre el error que ocurrió.

El control de los errores de la capa de RPC significaría que cada llamada a una biblioteca en zona de pruebas estaría seguida de una verificación adicional de la capa de RPC de la SAPI. Para abordar esas situaciones excepcionales, SAPI proporciona el módulo SAPI Transaction (transaction.h). Este módulo contiene la clase ::sapi::Transaction y garantiza que todas las llamadas a funciones de la biblioteca en zona de pruebas se completaron sin problemas a nivel de RPC o que se devolvió un error pertinente.

Transacción de SAPI

La SAPI aísla el código del host de la biblioteca en zona de pruebas y le brinda al llamador la capacidad de reiniciar o anular la solicitud de procesamiento de datos problemática. La transacción de la SAPI va un paso más allá y repite automáticamente los procesos fallidos.

Las transacciones de la API de SAPI se pueden usar de dos maneras diferentes: heredando directamente de ::sapi::Transaction o usando punteros de función que se pasan a ::sapi::BasicTransaction.

Las transacciones de la SAPI se definen anulando las siguientes tres funciones:

Métodos de transacción de la SAPI
::sapi::Transaction::Init() Esto es similar a llamar a un método de inicialización de una biblioteca típica de C/C++. Solo se llama al método una vez durante cada transacción a la biblioteca en zona de pruebas, a menos que se reinicie la transacción. En el caso de un reinicio, se vuelve a llamar al método, independientemente de cuántos reinicios hayan ocurrido antes.
::sapi::Transaction::Main() Se llama al método para cada llamada a ::sapi::Transaction::Run() .
::sapi::Transaction::Finish() Esto es similar a llamar a un método de limpieza de una biblioteca típica de C/C++. Se llama al método solo una vez durante la destrucción del objeto de transacción de la SAPI.

Uso normal de la biblioteca

En un proyecto sin bibliotecas en zona de pruebas, el patrón habitual cuando se trabaja con bibliotecas se ve de la siguiente manera:

LibInit();
while (data = NextDataToProcess()) {
  result += LibProcessData(data);
}
LibClose();

Se inicializa la biblioteca, luego se usan las funciones exportadas de la biblioteca y, por último, se llama a una función de cierre o finalización para limpiar el entorno.

Uso de bibliotecas en zonas de pruebas

En un proyecto con bibliotecas en zona de pruebas, el código de Uso normal de la biblioteca se traduce en el siguiente fragmento de código cuando se usan transacciones con devoluciones de llamada:

// Overwrite the Init method, passed to BasicTransaction initialization
::absl::Status Init(::sapi::Sandbox* sandbox) {
  // Instantiate the SAPI Object
  LibraryAPI lib(sandbox);
  // Instantiate the Sandboxed Library
  SAPI_RETURN_IF_ERROR(lib.LibInit());
  return ::absl::OkStatus();
}

// Overwrite the Finish method, passed to BasicTransaction initialization
::absl::Status Finish(::sapi::Sandbox *sandbox) {
  // Instantiate the SAPI Object
  LibraryAPI lib(sandbox);
  // Clean-up sandboxed library instance
  SAPI_RETURN_IF_ERROR(lib.LibClose());
  return ::absl::OkStatus();
}

// Wrapper function to process data, passed to Run method call
::absl::Status HandleData(::sapi::Sandbox *sandbox, Data data_to_process,
                           Result *out) {
  // Instantiate the SAPI Object
  LibraryAPI lib(sandbox);
  // Call the sandboxed function LibProcessData
  SAPI_ASSIGN_OR_RETURN(*out, lib.LibProcessData(data_to_process));
  return ::absl::OkStatus();
}

void Handle() {
  // Use SAPI Transactions by passing function pointers to ::sapi::BasicTransaction
  ::sapi::BasicTransaction transaction(Init, Finish);
  while (data = NextDataToProcess()) {
    ::sandbox2::Result result;
    // call the ::sapi::Transaction::Run() method
    transaction.Run(HandleData, data, &result);
    // ...
  }
  // ...
}

La clase de transacción se asegura de reinicializar la biblioteca en caso de que se produzca un error durante la invocación de handle_data. Veremos más sobre esto en la siguiente sección.

Reinicios de transacciones

Si una llamada a la API de la biblioteca en zona de pruebas genera un error durante la ejecución de los métodos de transacción de la SAPI (consulta la tabla anterior), se reiniciará la transacción. La cantidad predeterminada de reinicios se define con kDefaultRetryCnt en transaction.h.

Estos son algunos ejemplos de errores que se generan y que activarán un reinicio:

  • Se produjo un incumplimiento en la zona de pruebas
  • Se produjo un bloqueo en el proceso de zona de pruebas
  • Una función en zona de pruebas devolvió un código de error debido a un error de biblioteca

El procedimiento de reinicio observa el flujo normal de Init() y Main(), y, si las llamadas repetidas al método ::sapi::Transaction::Run() muestran errores, todo el método muestra un error a su llamador.

Manejo de errores de zona de pruebas o RPC

La interfaz de biblioteca con zona de pruebas generada automáticamente intenta ser lo más parecida posible al prototipo de función de biblioteca original de C/C++. Sin embargo, la biblioteca en zona de pruebas debe poder indicar cualquier error de zona de pruebas o RPC.

Esto se logra utilizando tipos de devolución ::sapi::StatusOr<T> (o ::sapi::Status para las funciones que devuelven void), en lugar de devolver directamente el valor de devolución de las funciones en zona de pruebas.

Además, SAPI proporciona algunas macros convenientes para verificar un objeto SAPI Status y reaccionar ante él. Estas macros se definen en el archivo de encabezado status_macro.h.

El siguiente fragmento de código es un extracto del ejemplo de suma y muestra el uso del estado de la SAPI y las macros:

// Instead of void, use ::sapi::Status
::sapi::Status SumTransaction::Main() {
  // Instantiate the SAPI Object
  SumApi f(sandbox());

  // ::sapi::StatusOr<int> sum(int a, int b)
  SAPI_ASSIGN_OR_RETURN(int v, f.sum(1000, 337));
  // ...

  // ::sapi::Status sums(sapi::v::Ptr* params)
  SumParams params;
  params.mutable_data()->a = 1111;
  params.mutable_data()->b = 222;
  params.mutable_data()->ret = 0;
  SAPI_RETURN_IF_ERROR(f.sums(params.PtrBoth()));
  // ...
  // Gets symbol address and prints its value
  int *ssaddr;
  SAPI_RETURN_IF_ERROR(sandbox()->Symbol(
      "sumsymbol", reinterpret_cast<void**>(&ssaddr)));
  ::sapi::v::Int sumsymbol;
  sumsymbol.SetRemote(ssaddr);
  SAPI_RETURN_IF_ERROR(sandbox()->TransferFromSandboxee(&sumsymbol));
  // ...
  return ::sapi::OkStatus();
}

Reinicio de la zona de pruebas

Muchas bibliotecas de zona de pruebas controlan la entrada sensible del usuario. Si la biblioteca en zona de pruebas se daña en algún momento y almacena datos entre ejecuciones, estos datos sensibles estarán en riesgo. Por ejemplo, si una versión en zona de pruebas de la biblioteca de Imagemagick comienza a enviar imágenes de la ejecución anterior.

Para evitar esta situación, el sandbox no se debe reutilizar para varias ejecuciones. Para detener la reutilización de zonas de pruebas, el código del host puede iniciar un reinicio del proceso de la biblioteca en zona de pruebas con ::sapi::Sandbox::Restart() o ::sapi::Transaction::Restart() cuando se usan transacciones de la SAPI.

Si se reinicia, se invalidará cualquier referencia al proceso de la biblioteca en zona de pruebas. Esto significa que los descriptores de archivos pasados o la memoria asignada ya no existirán.