mongodb / mongodb/mongo-php-driver

foreach over ObjectId and Garbage Collection

Open
#1,776 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

tracked-in-jira
Dominant language
PHP
Stars
923
Forks
218
Avg merge
15h 15m
Merged PRs (30d)
32

Description

Bug Report

foreach over ObjectId changes behavior after circular reference collector.

Before GC foreach over ObjectId executes one loop iteration with $key="oid".
After GC foreach over ObjectId executes zero loop iterations.

Environment

PHP inside Docker on Windows 10.
Docker Image: php:8.2.27-apache
PHP: 8.2.27
MongoDB PHP Driver: 1.20.1
MongoDB PHP Library: 1.20.0
MongoDB: 8.0.0
mongodb/laravel-mongodb: 4.8.1

MongoDB: I run DB in separate Docker container locally and use in PHP only HOST and PORT of DB.

www-data@5c2174446320:~/html$ php -i | grep -E 'mongodb|libmongoc|libbson'
Cannot load Xdebug - it was already loaded
/usr/local/etc/php/conf.d/mongodb.ini,
mongodb
libbson bundled version => 1.28.1
libmongoc bundled version => 1.28.1
libmongoc SSL => enabled
libmongoc SSL library => OpenSSL
libmongoc crypto => enabled
libmongoc crypto library => libcrypto
libmongoc crypto system profile => disabled
libmongoc SASL => enabled
libmongoc SRV => enabled
libmongoc compression => enabled
libmongoc compression snappy => enabled
libmongoc compression zlib => enabled
libmongoc compression zstd => enabled
libmongocrypt bundled version => 1.11.0
libmongocrypt crypto => enabled
libmongocrypt crypto library => libcrypto
mongodb.debug => no value => no value
Test Script

I've used mongodb/laravel-mongodb to reproduce the bug but there is not so much Laravel specific and main issue in \MongoDB\BSON\ObjectId.

ObjectIdTestModel.php

<?php

namespace App\Models;

class ObjectIdTestModel extends \MongoDB\Laravel\Eloquent\Model
{
}

Test.php

\dump();

// Uncommenting following line fixes the test
//gc_disable();

$objectId1 = new ObjectId();
$objectId2 = new ObjectId();

// Here we see public property "oid"
echo 'objectId1: ';
xdebug_debug_zval('objectId1');
echo 'objectId2: ';
xdebug_debug_zval('objectId2');

// Check possible assumption about iterating
$is_traversable = ($objectId1 instanceof \Traversable);
echo 'is_traversable: ' . var_export($is_traversable, true) . "\n";

// Check there are no properties for iterating in foreach hence there is not property "oid"
$reflect    = new \ReflectionClass($objectId1);
$properties = $reflect->getProperties();
echo 'properties: ' . print_r($properties, true);

// Allocate a lot of objects
for ($i = 0; $i < 200; $i++) {
    $model = new ObjectIdTestModel();
    $model->save();
}
$a = ObjectIdTestModel::all();

// First foreach over \MongoDB\BSON\ObjectId
$number1 = 0;
foreach ($objectId1 as $key => $value) {
    $number1 += 1;
}

// Allocate a lot of objects again
$a = ObjectIdTestModel::all();

// Second foreach over \MongoDB\BSON\ObjectId
$number2 = 0;
foreach ($objectId2 as $key => $value) {
    $number2 += 1;
}

echo 'objectId1: ';
xdebug_debug_zval('objectId1');
echo 'objectId2: ';
xdebug_debug_zval('objectId2');

$this->assertEquals(1, $number1, 'number1');
$this->assertEquals(1, $number2, 'number2');

Test output:

objectId1: objectId1: (refcount=1, is_ref=0)=class MongoDB\BSON\ObjectId { public $oid = (refcount=1, is_ref=0)='679a97b2d1cc47db6503f004' }
objectId2: objectId2: (refcount=1, is_ref=0)=class MongoDB\BSON\ObjectId { public $oid = (refcount=1, is_ref=0)='679a97b2d1cc47db6503f005' }
is_traversable: false
properties: Array
(
)
objectId1: objectId1: (refcount=1, is_ref=0)=class MongoDB\BSON\ObjectId { public $oid = (refcount=1, is_ref=0)='679a97b2d1cc47db6503f004' }
objectId2: objectId2: (refcount=1, is_ref=0)=class MongoDB\BSON\ObjectId { public $oid = (refcount=1, is_ref=0)='679a97b2d1cc47db6503f005' }

...

  number2
  Failed asserting that 0 matches expected 1.

If the bug is not repoduced you can increase number of created objects in line for ($i = 0; $i < 200; $i++) {

Expected and Actual Behavior
Expected

$number1 === 1
$number2 === 1
Circular reference collector do not change script behavior.

Actual

$number1 === 1
$number2 === 0
Calling gc_disable() "fixes" the bug.

Preffered solution

Prohibit to iterate over \MongoDB\BSON\ObjectId especially by hidden public property "oid".
I have iterated it by mistake and prefer empty loop over object without public properties instead of strange behavior.

Debug Log

object_id_gc.zip

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the behavior with the Test.php script and the MongoDB\BSON\ObjectId entry point, comparing foreach results before and after garbage collection. Review the ObjectId implementation and its interaction with PHP's circular reference collector; done means iteration behavior is consistent and matches the expected assertions without relying on gc_disable().

Written by the indexing model from the issue text.

Assessment

Tech stack
mongodb, php
Domain
backend, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.