HEX
Server: Apache/2.4.58 (Ubuntu)
System: Linux Bradford-Sitios 6.14.0-1017-azure #17~24.04.1-Ubuntu SMP Mon Dec 1 20:10:50 UTC 2025 x86_64
User: www-data (33)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /var/www/dtw.bradford/app/Imports/AbstractChunksImport backup.php
<?php

namespace App\Imports;

use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\AfterImport;

abstract class AbstractChunksImport implements ToCollection, WithHeadingRow, WithMultipleSheets, WithChunkReading, WithEvents
{
    /**
    * 🔧 OPTIMIZACIÓN DE IMPORTACIÓN: AJUSTES SEGÚN RECURSOS DEL SERVIDOR
    *
    * Esta clase implementa una importación por chunks, con escritura temporal en disco
    * para evitar sobrecarga de memoria y CPU. Puedes ajustar los siguientes parámetros
    * según las capacidades de tu servidor.
    *
    * 📌 Parámetros clave:
    *
    * 1. chunkSize() → 100
    *    - Nº de filas procesadas por lote (para lectura de Excel).
    *    - Aumentar (150–300): si el servidor tiene ≥ 4 núcleos y >8GB RAM.
    *    - Mantener (100): ideal para servidores con 2 núcleos y ~8GB RAM.
    *    - Reducir (50–80): si hay cuellos de botella de CPU o poca memoria.
    *
    * 2. $maxChunkSize → 500
    *    - Registros mantenidos en memoria antes de escribir a archivo temporal.
    *    - Aumentar (1000+): si tienes mucha RAM libre.
    *    - Reducir (100–300): si ves uso elevado de memoria.
    *
    * 3. $insertChunkSize → 100
    *    - Registros por lote al insertar en la base de datos.
    *    - Aumentar (200–500): si la BD es rápida y estable.
    *    - Reducir (50–80): si notas latencia alta en BD o errores por tiempo.
    *
    * 4. Pausas programadas:
    *    - Cada 10 chunks: `usleep(100000)` (100ms).
    *    - Cada 500 filas: `usleep(30000)` (30ms).
    *    - Estas pausas alivian la carga de CPU. Puedes reducirlas si el servidor está desocupado.
    *
    * 🧠 Recomendaciones:
    * - Monitorea con `htop`, `free -h`, y laravel.log durante importación.
    * - Ajusta valores de a poco, según el tamaño de tus archivos reales.
    * - Considera agregar SWAP si trabajas con archivos muy grandes (>500k filas).
    */

    public $errorMessage;
    public $successMessage;
    public $register_id;
    public $error_data = [];
    public $records_processed = 0;
    public $realHeaders;
    protected $chunkOffset = 0;
    protected $hasErrors = false;
    protected $headersValidated = false;

    protected $tempFile;
    protected $tempFilePath;
    protected $currentChunk = [];
    protected $maxChunkSize = 500;
    protected $insertChunkSize = 300;

    public function __construct()
    {
        $this->tempFilePath = tempnam(sys_get_temp_dir(), 'laravel_import_');
        $this->tempFile = fopen($this->tempFilePath, 'w+');
    }

    abstract protected function getTableName(): string;
    abstract protected function getExpectedHeaders(): array;
    abstract protected function getSheetName(); // string|int
    abstract protected function normalizeRow(array $row, int $numRow): ?array;
    protected int $headingRowIndex = 1; // La fila donde están los encabezados

    public function sheets(): array
    {
        return [$this->getSheetName() => $this];
    }

    public function headingRow(): int
    {
        return $this->headingRowIndex;
    }

    public function setChunkOffset(int $offset) { $this->chunkOffset = $offset; }
    public function chunkOffset(): int { return $this->chunkOffset; }

    public function collection(Collection $rows)
    {
        Log::info("[IMPORT] Procesando chunk offset: {$this->chunkOffset} - Filas: " . count($rows));

        if ($this->chunkOffset % 10 === 0) {
            usleep(100000);
        }

        if ($rows->isEmpty()) {
            $this->errorMessage = ['type' => 'ERROR DE VALIDACIÓN', 'message' => 'El documento está vacío'];
            $this->hasErrors = true;
            throw new Exception("El documento está vacío");
        }

        if (!$this->headersValidated) {
            $this->realHeaders = $rows->first()->keys()->toArray();
            Log::info("Headers del documento: " . json_encode($this->realHeaders));

            foreach ($this->getExpectedHeaders() as $header) {
                if (!in_array($header, $this->realHeaders)) {
                    $this->errorMessage = [
                        'type' => 'ERROR DE VALIDACIÓN',
                        'message' => 'Falta el encabezado requerido: ' . $header
                    ];
                    $this->hasErrors = true;
                    throw new Exception("Falta el encabezado requerido: " . $header);
                    return;
                }
            }

            $this->headersValidated = true;
        }

        $this->records_processed += count($rows);
        $num_row = $this->chunkOffset + 2;

        foreach ($rows as $row) {
            if ($num_row % 500 === 0) {
                usleep(30000);
                Log::info("Fila $num_row - Memoria: " . round(memory_get_usage() / 1024 / 1024, 2) . ' MB');
            }

            try {
                $normalized = $this->normalizeRow($row->toArray(), $num_row);

                if ($normalized) {
                    $this->currentChunk[] = $normalized;

                    if (count($this->currentChunk) >= $this->maxChunkSize) {
                        $this->writeChunkToTempFile();
                    }
                }
            } catch (\Exception $e) {
                $this->hasErrors = true;
                $this->error_data[] = [
                    'numero_de_linea' => $num_row,
                    'errores' => $e->getMessage(),
                    'fecha_de_carga' => date('d-m-Y H:i'),
                ];
            }

            unset($row);
            $num_row++;
        }

        if (!empty($this->currentChunk)) {
            Log::info("[IMPORT] Escritura final de chunk pendiente antes de finishImport()");
            $this->writeChunkToTempFile();
        }
    }

    protected function writeChunkToTempFile()
    {
        if (!empty($this->currentChunk)) {
            try {
                fwrite($this->tempFile, json_encode($this->currentChunk, JSON_UNESCAPED_UNICODE) . PHP_EOL);
                Log::info("[IMPORT] Se escribieron " . count($this->currentChunk) . " registros al archivo temporal");
            } catch (\Throwable $e) {
                Log::error("[IMPORT] Error al serializar chunk: " . $e->getMessage());
                Log::error("[IMPORT] Dump del chunk: " . json_encode($this->currentChunk));
                throw $e;
            }

            $this->currentChunk = [];
            gc_collect_cycles();
        } else {
            Log::info("[IMPORT] CurrentChunk vacío");
        }
    }


    protected function readFromTempFile()
    {
        rewind($this->tempFile);
        while (!feof($this->tempFile)) {
            $line = fgets($this->tempFile);
            if ($line !== false) {
                yield json_decode(trim($line), true);
            }
        }
    }

    public function chunkSize(): int
    {
        return 200;
    }

    public function registerEvents(): array
    {
        return [
            AfterImport::class => function (AfterImport $event) {
                Log::info('[IMPORT] Evento AfterImport ejecutado: todos los chunks han sido procesados');
            },
        ];
    }

    public function finishImport(): void
    {
        Log::info("[DEBUG] Entrando en finishImport() en tiempo real: " . now());

        try {
            if (!$this->hasErrors) {
                $start = microtime(true);
                $totalInserted = 0;
                $chunkCounter = 0;

                Log::info("Procediendo a leer archivo temporal por lotes...");

                foreach ($this->readFromTempFile() as $fileChunk) {
                    Log::info("[IMPORT] Leyendo chunk desde archivo temporal con " . count($fileChunk) . " registros");

                    if (!empty($fileChunk)) {
                        foreach (array_chunk($fileChunk, $this->insertChunkSize) as $insertChunk) {
                            Log::info("[IMPORT] Insertando chunk de " . count($insertChunk) . " registros");
                            DB::table($this->getTableName())->insert($insertChunk);
                            $totalInserted += count($insertChunk);
                            $chunkCounter++;
                            Log::info("[IMPORT] Insert chunk $chunkCounter completado. Memoria actual: " . round(memory_get_usage() / 1024 / 1024, 2) . " MB");
                            unset($insertChunk);
                            gc_collect_cycles();
                        }
                    }

                    unset($fileChunk);
                    gc_collect_cycles();
                }

                $time = round(microtime(true) - $start, 2);
                Log::info("[IMPORT] Inserción final completa. Total: $totalInserted registros en $time segundos");

                $this->successMessage = 'Importación realizada correctamente.';
                Log::info("[IMPORT] finishImport() completado correctamente");

            } elseif (!empty($this->error_data)) {
                $this->errorMessage = [
                    'type' => 'ERROR DE VALIDACIÓN',
                    'message' => 'Error de validación de datos. Revise informe de carga.'
                ];
            } else {
                $this->errorMessage = [
                    'type' => 'ERROR DE SISTEMA',
                    'message' => 'Ocurrió un error inesperado durante la importación.'
                ];
                Log::info("[IMPORT] Error desconocido en finishImport()");
            }
        } catch (\Exception $e) {
            $this->errorMessage = [
                'type' => 'ERROR DE BASE DE DATOS',
                'message' => 'Error al insertar datos: ' . $e->getMessage()
            ];
            $this->hasErrors = true;
            Log::error("[IMPORT] Excepción capturada en finishImport: " . $e->getMessage());
        } finally {
            if (is_resource($this->tempFile)) {
                fclose($this->tempFile);
                $this->tempFile = null;
            }
            if (file_exists($this->tempFilePath)) {
                unlink($this->tempFilePath);
                $this->tempFilePath = null;
            }
        }
    }
}