Skip to content

Commit 3604443

Browse files
Remove Remote Config dependency on Long JS (#2614)
The conditional logic used by the Remote Config config fetch endpoint hashes randomization IDs into 64 bit numbers (via an algorithm called Farmhash). JS numbers are limited to 53 bits, so we used the long.js package to emulate the fetch endpoint logic in the Admin SDK. PR 2603 migrated the Admin SDK to use a Farmhash library that produces BigInt, which supports 64 bit numbers natively, so we can remove the long.js dependency. Additionally, the SDK currently converts the BigInt to a string before converting the string to a longjs object, which is clunky; and Long JS broke the SDK's ES6 compatibility. --------- Co-authored-by: Lahiru Maramba <[email protected]>
1 parent 3049575 commit 3604443

File tree

4 files changed

+97
-17
lines changed

4 files changed

+97
-17
lines changed

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@
203203
"farmhash-modern": "^1.1.0",
204204
"jsonwebtoken": "^9.0.0",
205205
"jwks-rsa": "^3.1.0",
206-
"long": "^5.2.3",
207206
"node-forge": "^1.3.1",
208207
"uuid": "^10.0.0"
209208
},

src/remote-config/condition-evaluator-internal.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
PercentConditionOperator
2727
} from './remote-config-api';
2828
import * as farmhash from 'farmhash-modern';
29-
import long = require('long');
3029

3130
/**
3231
* Encapsulates condition evaluation logic to simplify organization and
@@ -147,26 +146,18 @@ export class ConditionEvaluator {
147146
const seedPrefix = seed && seed.length > 0 ? `${seed}.` : '';
148147
const stringToHash = `${seedPrefix}${context.randomizationId}`;
149148

149+
const hash64 = ConditionEvaluator.hashSeededRandomizationId(stringToHash)
150150

151-
// Using a 64-bit long for consistency with the Remote Config fetch endpoint.
152-
let hash64 = long.fromString(farmhash.fingerprint64(stringToHash).toString());
151+
const instanceMicroPercentile = hash64 % BigInt(100 * 1_000_000);
153152

154-
// Negate the hash if its value is less than 0. We handle this manually because the
155-
// Long library doesn't provided an absolute value method.
156-
if (hash64.lt(0)) {
157-
hash64 = hash64.negate();
158-
}
159-
160-
const instanceMicroPercentile = hash64.mod(100 * 1_000_000);
161-
162153
switch (percentOperator) {
163154
case PercentConditionOperator.LESS_OR_EQUAL:
164-
return instanceMicroPercentile.lte(normalizedMicroPercent);
155+
return instanceMicroPercentile <= normalizedMicroPercent;
165156
case PercentConditionOperator.GREATER_THAN:
166-
return instanceMicroPercentile.gt(normalizedMicroPercent);
157+
return instanceMicroPercentile > normalizedMicroPercent;
167158
case PercentConditionOperator.BETWEEN:
168-
return instanceMicroPercentile.gt(normalizedMicroPercentLowerBound)
169-
&& instanceMicroPercentile.lte(normalizedMicroPercentUpperBound);
159+
return instanceMicroPercentile > normalizedMicroPercentLowerBound
160+
&& instanceMicroPercentile <= normalizedMicroPercentUpperBound;
170161
case PercentConditionOperator.UNKNOWN:
171162
default:
172163
break;
@@ -175,4 +166,20 @@ export class ConditionEvaluator {
175166
// TODO: add logging once we have a wrapped logger.
176167
return false;
177168
}
169+
170+
// Visible for testing
171+
static hashSeededRandomizationId(seededRandomizationId: string): bigint {
172+
// For consistency with the Remote Config fetch endpoint's percent condition behavior
173+
// we use Farmhash's fingerprint64 algorithm and interpret the resulting unsigned value
174+
// as a signed value.
175+
let hash64 = BigInt.asIntN(64, farmhash.fingerprint64(seededRandomizationId));
176+
177+
// Manually negate the hash if its value is less than 0, since Math.abs doesn't
178+
// support BigInt.
179+
if (hash64 < 0) {
180+
hash64 = -hash64;
181+
}
182+
183+
return hash64;
184+
}
178185
}

test/unit/remote-config/condition-evaluator.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,4 +858,77 @@ describe('ConditionEvaluator', () => {
858858
}
859859
});
860860
});
861+
862+
describe('hashSeededRandomizationId', () => {
863+
// The Farmhash algorithm produces a 64 bit unsigned integer,
864+
// which we convert to a signed integer for legacy compatibility.
865+
// This has caused confusion in the past, so we explicitly
866+
// test here.
867+
it('should leave numbers <= 2^63-1 (max signed long) as is', function () {
868+
if (nodeVersion.startsWith('14')) {
869+
this.skip();
870+
}
871+
872+
const stub = sinon
873+
.stub(farmhash, 'fingerprint64')
874+
// 2^63-1 = 9223372036854775807.
875+
.returns(BigInt('9223372036854775807'));
876+
stubs.push(stub);
877+
878+
const actual = ConditionEvaluator.hashSeededRandomizationId('anything');
879+
880+
expect(actual).to.equal(BigInt('9223372036854775807'))
881+
});
882+
883+
it('should convert 2^63 to negative (min signed long) and then find the absolute value', function () {
884+
if (nodeVersion.startsWith('14')) {
885+
this.skip();
886+
}
887+
888+
const stub = sinon
889+
.stub(farmhash, 'fingerprint64')
890+
// 2^63 = 9223372036854775808.
891+
.returns(BigInt('9223372036854775808'));
892+
stubs.push(stub);
893+
894+
const actual = ConditionEvaluator.hashSeededRandomizationId('anything');
895+
896+
// 2^63 is the negation of 2^63-1
897+
expect(actual).to.equal(BigInt('9223372036854775808'))
898+
});
899+
900+
it('should convert 2^63+1 to negative and then find the absolute value', function () {
901+
if (nodeVersion.startsWith('14')) {
902+
this.skip();
903+
}
904+
905+
const stub = sinon
906+
.stub(farmhash, 'fingerprint64')
907+
// 2^63+1 9223372036854775809.
908+
.returns(BigInt('9223372036854775809'));
909+
stubs.push(stub);
910+
911+
const actual = ConditionEvaluator.hashSeededRandomizationId('anything');
912+
913+
// 2^63+1 is larger than 2^63, so the absolute value is smaller
914+
expect(actual).to.equal(BigInt('9223372036854775807'))
915+
});
916+
917+
it('should handle the value that initially caused confusion', function () {
918+
if (nodeVersion.startsWith('14')) {
919+
this.skip();
920+
}
921+
922+
const stub = sinon
923+
.stub(farmhash, 'fingerprint64')
924+
// We were initially confused about the nature of this value ...
925+
.returns(BigInt('16081085603393958147'));
926+
stubs.push(stub);
927+
928+
const actual = ConditionEvaluator.hashSeededRandomizationId('anything');
929+
930+
// ... Now we know it's the unsigned equivalent of this absolute value.
931+
expect(actual).to.equal(BigInt('2365658470315593469'))
932+
});
933+
});
861934
});

0 commit comments

Comments
 (0)