php-resque icon indicating copy to clipboard operation
php-resque copied to clipboard

Objects passed to workers via arguments get converted into associative arrays

Open jamesmoss opened this issue 12 years ago • 1 comments

In the job() method of Worker the json representing the job's arguments gets decoded from redis using:

return json_decode($job, true);

Passing true as the second parameter to json_decode means that any objects passed as arguments to the job get destroyed and turned into arrays.

Is there any reason for this happening? Surely ensuring that the data is the same between the object creating the job and its worker is pretty important. This caused a few headaches on a recent project trying to work out why objects were turning into arrays in workers.

It looks like this code has been in place for 4 years so changing it might break backwards compatibility. Can we add it as an option on the worker?

jamesmoss avatar Dec 17 '13 16:12 jamesmoss

Here's the deal. JSON, as the name implies, is a JavaScript-based notation. JS doesn't have a concept of "associative arrays" - it's an array, or it's an object. That's it. But you can still use array notation to set objects' properties. What's more, JS objects treat everything as properties - methods are just properties that contain functions instead of scalars, arrays, or other objects.

All this is fine with moving to PHP except the fact that JSON has no mechanism for encoding functions. This is actually one of its strengths as a cross-language data communication type. Not all functions written in JS can be translated into PHP - or vice versa - and many shouldn't. The logic required for such translations would be astoundingly complex - and slow. So methods, in effect, are skipped entirely. This leaves JSON objects as actually nothing more than associative arrays. (And in fact PHP has no choice but to encode associative arrays as JSON "objects" in the first place; JS terminology obscures JSON capabilities by calling an associative array an object. You can pass false as the second argument, but it's the same thing as casting all associative arrays to objects.)

So now we get back to PHP-Resque. One of the project's goals has been to maintain compatibility, at least within Redis, with the Ruby version, of which this project is a port. Among other things, such as not needing to reinvent the Ruby Resque Web interface, this allows jobs to be enqueued by one and run by either. (Say your enqueuer is a PHP application, but some of the jobs you want to run are better suited to Ruby - something that actually happens a lot in practice. You can add Ruby jobs to one queue, PHP jobs to another, and expect both to be able to process the jobs without the enqueuer ever caring which language is processing them.). Since JSON is such a huge part of working in Ruby, it's the obvious choice for storing job data in a text-only representation until it can be run later. That means JSON is what we use, as well. As a side effect, we sacrifice some of the richness of PHP's data types.

Now, we could easily extend the library so that jobs include a flag indicating whether to decode into arrays or objects, but if you have the flag in object mode (second argument to json_encode() is false), all of your associative arrays are suddenly objects. Including the one that we use to combine all the elements that comprise a job (class, arguments, ID, queue, and ostensibly this flag). This adds a layer of complexity to the implementation in that we'd have to reference these elements differently depending on the value of the flag, as well as not really being what we'd actually expect. This is especially complicated in light of JS not supporting arrays that skip indices. PHP supports these just fine, but as they pass through JSON, they become objects, in the JS sense. Definitely not expected/desired behavior. So we stick with associative decoding because it is the most consistent approach available - and even if we put objects in, they become no better than associative arrays when they come out, anyway.

Note, however, that this doesn't mean you should give up on passing objects, necessarily. It will just take a bit more work on either end. The first step is to convert the object you'd like to pass into an associative array, with one or more extra keys for data that an array can't reliably reproduce once reduced to JSON text. Most importantly, the class name. This will help reconstruct the object on the other end.

Then, probably using a hook into beforePerform, use that extra information on the worker side to reconstruct the object(s) you require. Use the class name to create a new object. Use the other keys to bring its state into line with what it was prior to enqueue. Then assign this object into the job array using the same key you started with - that is, replace the array version with the (new) object version. When your job's setUp(), perform(), and tearDown() are called, they'll have the object version instead of the array.

It requires a bit of extra work to pull it off, but the existing implementation can still support your needs. Ultimately, the way we're handling JSON decoding is the most consistent and useful approach available, so it isn't likely to change, even conditionally.

danhunsaker avatar Dec 17 '13 23:12 danhunsaker