• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PhpSpreadsheet / 22973523338

11 Mar 2026 08:42PM UTC coverage: 96.827% (-0.08%) from 96.904%
22973523338

Pull #4834

github

web-flow
Merge 7cf4952f8 into a1dacfdf7
Pull Request #4834: Add parallel Xlsx writing via pcntl_fork

154 of 198 new or added lines in 7 files covered. (77.78%)

17 existing lines in 1 file now uncovered.

47846 of 49414 relevant lines covered (96.83%)

382.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.16
/src/PhpSpreadsheet/Parallel/Backend/PcntlBackend.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Parallel\Backend;
4

5
use Closure;
6
use PhpOffice\PhpSpreadsheet\Exception;
7
use Throwable;
8

9
class PcntlBackend implements BackendInterface
10
{
11
    private const DEFAULT_TIMEOUT = 60;
12

13
    private int $timeout;
14

15
    public function __construct(int $timeout = self::DEFAULT_TIMEOUT)
456✔
16
    {
17
        $this->timeout = $timeout;
456✔
18
    }
19

20
    public function execute(array $tasks, Closure $worker, int $maxWorkers): array
12✔
21
    {
22
        if (!self::isAvailable()) {
12✔
NEW
23
            throw new Exception('pcntl extension is not available');
×
24
        }
25

26
        $taskCount = count($tasks);
12✔
27
        $results = array_fill(0, $taskCount, null);
12✔
28
        $tempFiles = [];
12✔
29
        $pids = [];
12✔
30
        $isChild = false;
12✔
31

32
        try {
33
            // Process tasks in batches of maxWorkers
34
            for ($batchStart = 0; $batchStart < $taskCount; $batchStart += $maxWorkers) {
12✔
35
                $batchEnd = min($batchStart + $maxWorkers, $taskCount);
12✔
36
                $batchPids = [];
12✔
37

38
                // Fork children for this batch
39
                for ($i = $batchStart; $i < $batchEnd; ++$i) {
12✔
40
                    $tempFile = tempnam(sys_get_temp_dir(), 'phpspreadsheet_parallel_');
12✔
41
                    if ($tempFile === false) {
12✔
NEW
42
                        throw new Exception('Failed to create temp file for parallel execution');
×
43
                    }
44
                    $tempFiles[$i] = $tempFile;
12✔
45

46
                    $pid = pcntl_fork();
12✔
47
                    if ($pid === -1) {
12✔
NEW
48
                        throw new Exception('Failed to fork process');
×
49
                    }
50

51
                    if ($pid === 0) {
12✔
52
                        // Child process
NEW
53
                        $isChild = true;
×
54

55
                        try {
NEW
56
                            $result = $worker($tasks[$i]);
×
NEW
57
                            file_put_contents($tempFile, serialize($result));
×
NEW
58
                        } catch (Throwable $e) {
×
NEW
59
                            file_put_contents($tempFile, serialize(
×
NEW
60
                                new ParallelTaskError($e->getMessage(), (int) $e->getCode())
×
NEW
61
                            ));
×
62
                        }
63
                        // @codeCoverageIgnoreStart
64
                        exit(0);
65
                        // @codeCoverageIgnoreEnd
66
                    }
67

68
                    // Parent process
69
                    $pids[$i] = $pid;
12✔
70
                    $batchPids[$i] = $pid;
12✔
71
                }
72

73
                // Wait for all children in this batch
74
                foreach ($batchPids as $i => $pid) {
12✔
75
                    $this->waitForChild($pid);
12✔
76
                }
77

78
                // Collect results for this batch
79
                foreach ($batchPids as $i => $pid) {
11✔
80
                    if (!isset($tempFiles[$i]) || !is_file($tempFiles[$i])) {
11✔
NEW
81
                        throw new Exception("Result file missing for task {$i}");
×
82
                    }
83

84
                    $content = file_get_contents($tempFiles[$i]);
11✔
85
                    if ($content === false) {
11✔
NEW
86
                        throw new Exception("Failed to read result for task {$i}");
×
87
                    }
88

89
                    $result = unserialize($content);
11✔
90
                    if ($result instanceof ParallelTaskError) {
11✔
91
                        throw new Exception("Parallel task {$i} failed: " . $result->getMessage());
1✔
92
                    }
93

94
                    $results[$i] = $result;
11✔
95
                }
96
            }
97
        } finally {
98
            // Only parent cleans up — child must not touch shared state
99
            if (!$isChild) {
12✔
100
                // Reap any remaining children
101
                foreach ($pids as $pid) {
12✔
102
                    pcntl_waitpid($pid, $status, WNOHANG);
12✔
103
                }
104

105
                // Clean up temp files
106
                foreach ($tempFiles as $file) {
12✔
107
                    if (is_file($file)) {
12✔
108
                        @unlink($file);
12✔
109
                    }
110
                }
111
            }
112
        }
113

114
        return array_values($results);
10✔
115
    }
116

117
    private function waitForChild(int $pid): void
12✔
118
    {
119
        $startTime = time();
12✔
120

121
        while (true) {
12✔
122
            $result = pcntl_waitpid($pid, $status, WNOHANG);
12✔
123

124
            if ($result === $pid) {
12✔
125
                return;
11✔
126
            }
127

128
            if ($result === -1) {
12✔
NEW
129
                return;
×
130
            }
131

132
            if ((time() - $startTime) >= $this->timeout) {
12✔
133
                // Attempt graceful termination
134
                if (function_exists('posix_kill')) {
1✔
135
                    posix_kill($pid, 15); // SIGTERM
1✔
136
                    usleep(100000); // 100ms grace period
1✔
137
                }
138
                pcntl_waitpid($pid, $status, WNOHANG);
1✔
139

140
                throw new Exception("Parallel task timed out after {$this->timeout} seconds");
1✔
141
            }
142

143
            usleep(10000); // 10ms poll interval
12✔
144
        }
145
    }
146

147
    public static function isAvailable(): bool
457✔
148
    {
149
        return function_exists('pcntl_fork')
457✔
150
            && function_exists('pcntl_waitpid')
457✔
151
            && PHP_OS_FAMILY !== 'Windows';
457✔
152
    }
153
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc