Skip to content

foreach over ObjectId and Garbage Collection #1776

Open
@uh-zuh

Description

@uh-zuh

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    tracked-in-jiraTicket filed in Mongo's Jira system

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions