Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RTDB persistence layer triggers a bug with number parsing #4108

Closed
mortenbekditlevsen opened this issue Oct 17, 2019 · 2 comments
Closed

RTDB persistence layer triggers a bug with number parsing #4108

mortenbekditlevsen opened this issue Oct 17, 2019 · 2 comments
Assignees

Comments

@mortenbekditlevsen
Copy link
Contributor

@mortenbekditlevsen mortenbekditlevsen commented Oct 17, 2019

UPDATED After digging around a bit more, I think that I have gotten closer to the actual issue. My findings are added in the bottom.

[READ] Step 1: Are you in the right place?

Yup, most definitely.

[REQUIRED] Step 2: Describe your environment

  • Xcode version: 10.3
  • Firebase SDK version: 6.2.0 (also present in 6.10.0)
  • Firebase Component: Database
  • Component version: 6.2.0

[REQUIRED] Step 3: Describe the problem

From the user perspective we wrote the value 1000 to the RTDB and when we read it back in we got -844.

What actually happened is that we wrote the value 999.9999999999999487.
Reading back this value through the persistence layer triggers an issue with NSDecimalNumber conversion.

When disabling persistence and reading/writing directly to the RTDB, this problem does not occur.

I started a discussion about the issue on the Swift forums:
https://forums.swift.org/t/nsdecimalnumber-conversion-error/29862/3

And I was directed to previous descriptions of the issue. Apparently it has been around for a long time.
http://www.openradar.me/radar?id=5007005597040640

Steps to reproduce:

Use Firebase RTDB with persistence enabled.
Write an NSDecimalNumber with many decimals of precision.
Read it back and check that the returned value is completely bonkers.
Turn off persistence and repeat to verify that it only happens with persistence enabled.

Add the following code to a Swift project to test.
Run twice to trigger the error. Then disable persistence and run again to see that the error is gone:

import UIKit
import FirebaseDatabase
import Firebase

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let db = Database.database()
        db.isPersistenceEnabled = true
        let ref = db.reference(withPath: "a")

        let decimal = Decimal(_exponent: -16, _length: 4, _isNegative: 0, _isCompact: 0, _reserved: 0, _mantissa: (65023, 35303, 8964, 35527, 0, 0, 0, 0))

        _ = ref.observeSingleEvent(of: .value) { snap in
            print("SNAP", snap)
        }

        ref.setValue(decimal as NSDecimalNumber)
    }
}

Relevant Code:

The code that triggers the issue is in FLevelDBStorageEngine.m:

- (id)fixDoubleParsing:(id)value
    __attribute__((no_sanitize("float-cast-overflow"))) {
    // The parser for double values in JSONSerialization at the root takes some
    // short-cuts and delivers wrong results (wrong rounding) for some double
    // values, including 2.47. Because we use the exact bytes for hashing on the
    // server this will lead to hash mismatches. The parser of NSNumber seems to
    // be more in line with what the server expects, so we use that here
    if ([value isKindOfClass:[NSNumber class]]) {
        CFNumberType type = CFNumberGetType((CFNumberRef)value);
        if (type == kCFNumberDoubleType || type == kCFNumberFloatType) {
            // The NSJSON parser returns all numbers as double values, even
            // those that contain no exponent. To make sure that the String
            // conversion below doesn't unexpectedly reduce precision, we make
            // sure that our number is indeed not an integer.
            if ((double)(int64_t)[value doubleValue] != [value doubleValue]) {
                NSString *doubleString = [value stringValue];
                return [NSNumber numberWithDouble:[doubleString doubleValue]];
            } else {
                return [NSNumber numberWithLongLong:[value longLongValue]];
            }
        }
    }
    return value;
}

The value is checked to see if it can be represented as an integer. Since the doubleValue is 1000, it can be represented as an integer.
But then a new number is created using the longLongValue of the same number, and this triggers the error and gives -844.

Update

I think that the core of the issue is, that if I try to save a double value like 2.47 (which cannot be represented directly in a double), then the RTDB stores it precisely as 2.47. At least that is what I see when I retrieve the value again.
BUT: If I wrap the double in an NSDecimalNumber, then the double value is represented 'as is' with all decimals: 2.470000000000000512

So if the persistence layer tries to fix up any values when reading them, basically it has no knowledge about whether 2.47 or 2.470000000000000512 is how the RTDB actually stores the value. And as such I guess that it cannot be certain to calculate the same hashes or produce the same results when fetching data.

If those suspicions are correct, then any 'fixing' of the data needs to be performed when writing the data into the persisence layer - and not when reading out the values.


Another solution is to disallow serializing values of the NSDecimalNumber subclass, but the RTDB documentation states that you can write NSNumbers, and that of course includes NSDecimalNumbers...

@schmidt-sebastian
Copy link
Member

@schmidt-sebastian schmidt-sebastian commented Oct 18, 2019

I will close this for now as #4109 was merged. If we need to improve our parsing further to match the hash codes the backend uses, then we can open a follow-up issue.

@schmidt-sebastian
Copy link
Member

@schmidt-sebastian schmidt-sebastian commented Oct 18, 2019

@mortenbekditlevsen Thank you for your contribution.

@firebase firebase locked and limited conversation to collaborators Nov 18, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
3 participants