Repository: appwrite/appwrite
File: src/Appwrite/Platform/Modules/Functions/Http/Executions/XList.php (PHP)

Code before:
            }

            $executionId = $cursor->getValue();
            $cursorDocument = $dbForProject->getDocument('executions', $executionId);

            if ($cursorDocument->isEmpty()) {
                throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Execution '{$executionId}' for the 'cursor' value not found.");
            }

            $cursor->setValue($cursorDocument);
        }

        $requestedStatuses = [];
        foreach ($queries as $query) {
            if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status') {
                $requestedStatuses = \array_merge($requestedStatuses, $query->getValues());
            }
        }

        if (\in_array('failed', $requestedStatuses)) {
            foreach ($queries as $query) {
                if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status') {
                    $query->setValues(\array_unique(\array_merge($query->getValues(), ['waiting', 'processing'])));
                }
            }
        }

        $filterQueries = Query::groupByType($queries)['filters'];

        try {
            $results = $dbForProject->find('executions', $queries);
            $total = $includeTotal ? $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT) : 0;
        } catch (OrderException $e) {
            throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
        }

        $timeout = $function->getAttribute('timeout', 900);
        foreach ($results as $execution) {
            $status = $execution->getAttribute('status', '');
            if ($status === 'waiting' || $status === 'processing') {
                $elapsed = \time() - \strtotime($execution->getCreatedAt());
                if ($elapsed >= $timeout) {
                    $execution->setAttribute('status', 'failed');
                }
            }
        }

        if (!empty($requestedStatuses)) {
            $results = \array_values(\array_filter($results, fn ($execution) => \in_array($execution->getAttribute('status', ''), $requestedStatuses)));
        }


Review comment (bug):
I think this would break pagination, due to need for array_filter below.

I think logic should be:

If status query exists and looks for failed deployment, override query

Before:
```
queries: [ Query.limit(5), Query.equal('status', 'failed') ]
```

After
```
queries: [
  Query.limit(5),
  Query.or([
    Query.equal('status', 'failed') ],
    Query.and([
      Query.equal('status', ['waiting', 'processing'],
      Query.lessThan('createdAt', now-15min),
    ]).
  ]),


```

Code after:
                throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
            }

            $executionId = $cursor->getValue();
            $cursorDocument = $dbForProject->getDocument('executions', $executionId);

            if ($cursorDocument->isEmpty()) {
                throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Execution '{$executionId}' for the 'cursor' value not found.");
            }

            $cursor->setValue($cursorDocument);
        }

        // Calculate the cutoff datetime before which a waiting/processing execution is considered timed out.
        $timeout = $function->getAttribute('timeout', 900);
        $thresholdDate = new \DateTime("-{$timeout} seconds");
        $threshold = DateTime::format($thresholdDate);

        // Capture what statuses the caller explicitly requested, before we mutate the query.
        $requestedStatuses = [];
        foreach ($queries as $query) {
            if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status') {
                $requestedStatuses = [...$requestedStatuses, ...$query->getValues()];
            }
        }

        // If the caller is filtering by 'failed', expand the DB query to also return
        // waiting/processing executions created before the timeout threshold, so timed-out
        // executions that were never marked failed in the DB are included in the results.
        foreach ($queries as $index => $query) {
            if ($query->getMethod() === Query::TYPE_EQUAL && $query->getAttribute() === 'status' && \in_array('failed', $query->getValues())) {
                $queries[$index] = Query::or([
                    $query,
                    Query::and([
                        Query::equal('status', ['waiting', 'processing']),
                        Query::createdBefore($threshold),
                    ]),
                ]);
                break;
            }
        }

        $filterQueries = Query::groupByType($queries)['filters'];

        try {
            $results = $dbForProject->find('executions', $queries);
            $total = $includeTotal ? $dbForProject->count('executions', $filterQueries, APP_LIMIT_COUNT) : 0;
        } catch (OrderException $e) {
            throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
        }