Skip to content

Avoid fetching dereferenced segments in server-side #414

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

Draft
wants to merge 4 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
2.4.1 (XXX XX, 2025)
- Updated synchronization to avoid fetching dereferenced segments in server-side, which were resulting in 404 response errors.

2.4.0 (May 27, 2025)
- Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK.
- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules.
Expand Down
11 changes: 6 additions & 5 deletions src/storages/AbstractMySegmentsCacheSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync
protected abstract addSegment(name: string): boolean
protected abstract removeSegment(name: string): boolean
protected abstract setChangeNumber(changeNumber?: number): boolean | void
protected abstract getSegments(): string[]

/**
* For server-side synchronizer: check if `key` is in `name` segment.
Expand All @@ -27,14 +28,14 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync


// No-op. Not used in client-side.
registerSegments(): boolean { return false; }
update() { return false; }

/**
* For server-side synchronizer: get the list of segments to fetch changes.
* Also used for the `seC` (segment count) telemetry stat.
* Used for the `seC` (segment count) telemetry stat.
*/
abstract getRegisteredSegments(): string[]
getSegmentsCount() {
return this.getSegments().length;
}

/**
* Only used for the `skC`(segment keys count) telemetry stat: 1 for client-side, and total count of keys in server-side.
Expand Down Expand Up @@ -68,7 +69,7 @@ export abstract class AbstractMySegmentsCacheSync implements ISegmentsCacheSync
}

const names = ((segmentsData as IMySegmentsResponse).k || []).map(s => s.n).sort();
const storedSegmentKeys = this.getRegisteredSegments().sort();
const storedSegmentKeys = this.getSegments().sort();

// Extreme fast => everything is empty
if (!names.length && !storedSegmentKeys.length) return false;
Expand Down
2 changes: 1 addition & 1 deletion src/storages/AbstractSplitsCacheAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync {
abstract trafficTypeExists(trafficType: string): Promise<boolean>
abstract clear(): Promise<boolean | void>

// @TODO revisit segment-related methods ('usesSegments', 'getRegisteredSegments', 'registerSegments')
// @TODO revisit segment-related methods ('usesSegments')
// noop, just keeping the interface. This is used by standalone client-side API only, and so only implemented by InMemory and InLocalStorage.
usesSegments(): Promise<boolean> {
return Promise.resolve(true);
Expand Down
4 changes: 0 additions & 4 deletions src/storages/KeyBuilderSS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ export class KeyBuilderSS extends KeyBuilder {
this.versionablePrefix = `${metadata.s}/${metadata.n}/${metadata.i}`;
}

buildRegisteredSegmentsKey() {
return `${this.prefix}.segments.registered`;
}

buildImpressionsKey() {
return `${this.prefix}.impressions`;
}
Expand Down
7 changes: 0 additions & 7 deletions src/storages/__tests__/KeyBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,8 @@ test('KEYS / segments keys', () => {
const expectedKey = `SPLITIO.segment.${segmentName}`;
const expectedTill = `SPLITIO.segment.${segmentName}.till`;

const expectedSegmentRegistered = 'SPLITIO.segments.registered';

expect(builder.buildSegmentNameKey(segmentName) === expectedKey).toBe(true);
expect(builder.buildSegmentTillKey(segmentName) === expectedTill).toBe(true);
expect(builder.buildRegisteredSegmentsKey() === expectedSegmentRegistered).toBe(true);

// NOT USED
// const expectedReady = 'SPLITIO.segments.ready';
// expect(builder.buildSegmentsReady() === expectedReady).toBe(true);
});

test('KEYS / traffic type keys', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/storages/inLocalStorage/MySegmentsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class MySegmentsCacheInLocal extends AbstractMySegmentsCacheSync {
return localStorage.getItem(this.keys.buildSegmentNameKey(name)) === DEFINED;
}

getRegisteredSegments(): string[] {
protected getSegments(): string[] {
// Scan current values from localStorage
return Object.keys(localStorage).reduce((accum, key) => {
let segmentName = this.keys.extractSegmentName(key);
Expand Down
4 changes: 4 additions & 0 deletions src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync {
return item && JSON.parse(item);
}

getAll(): IRBSegment[] {
return this.getNames().map(name => this.get(name) as IRBSegment);
}

contains(names: Set<string>): boolean {
const namesArray = setToArray(names);
const namesInStorage = this.getNames();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ test('SEGMENT CACHE / in LocalStorage', () => {
expect(cache.getChangeNumber()).toBe(-1);

expect(cache.isInSegment('mocked-segment')).toBe(true);
expect(cache.getRegisteredSegments()).toEqual(['mocked-segment', 'mocked-segment-2']);
expect(cache.getKeysCount()).toBe(1);
});

Expand All @@ -29,7 +28,6 @@ test('SEGMENT CACHE / in LocalStorage', () => {
});

expect(cache.isInSegment('mocked-segment')).toBe(false);
expect(cache.getRegisteredSegments()).toEqual(['mocked-segment-2']);
expect(cache.getKeysCount()).toBe(1);
});

Expand Down
2 changes: 1 addition & 1 deletion src/storages/inMemory/MySegmentsCacheInMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class MySegmentsCacheInMemory extends AbstractMySegmentsCacheSync {
return this.cn || -1;
}

getRegisteredSegments() {
protected getSegments() {
return Object.keys(this.segmentCache);
}

Expand Down
4 changes: 4 additions & 0 deletions src/storages/inMemory/RBSegmentsCacheInMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync {
return this.cache[name] || null;
}

getAll(): IRBSegment[] {
return this.getNames().map(name => this.get(name) as IRBSegment);
}

contains(names: Set<string>): boolean {
const namesArray = setToArray(names);
const namesInStorage = this.getNames();
Expand Down
20 changes: 2 additions & 18 deletions src/storages/inMemory/SegmentsCacheInMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,8 @@ export class SegmentsCacheInMemory implements ISegmentsCacheSync {
this.segmentChangeNumber = {};
}

private _registerSegment(name: string) {
if (!this.segmentCache[name]) {
this.segmentCache[name] = new Set<string>();
}

return true;
}

registerSegments(names: string[]) {
for (let i = 0; i < names.length; i++) {
this._registerSegment(names[i]);
}

return true;
}

getRegisteredSegments() {
return Object.keys(this.segmentCache);
getSegmentsCount() {
return Object.keys(this.segmentCache).length;
}

getKeysCount() {
Expand Down
4 changes: 2 additions & 2 deletions src/storages/inMemory/TelemetryCacheInMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ export class TelemetryCacheInMemory implements ITelemetryCacheSync {
iDe: this.getImpressionStats(DEDUPED),
iDr: this.getImpressionStats(DROPPED),
spC: this.splits && this.splits.getSplitNames().length,
seC: this.segments && this.segments.getRegisteredSegments().length,
seC: this.segments && this.segments.getSegmentsCount(),
skC: this.segments && this.segments.getKeysCount(),
lsC: this.largeSegments && this.largeSegments.getRegisteredSegments().length,
lsC: this.largeSegments && this.largeSegments.getSegmentsCount(),
lskC: this.largeSegments && this.largeSegments.getKeysCount(),
sL: this.getSessionLength(),
eQ: this.getEventStats(QUEUED),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,13 @@ test('MY SEGMENTS CACHE / in memory', () => {
expect(cache.getChangeNumber()).toBe(-1);

expect(cache.isInSegment('mocked-segment')).toBe(true);
expect(cache.getRegisteredSegments()).toEqual(['mocked-segment', 'mocked-segment-2']);
expect(cache.getKeysCount()).toBe(1);

expect(cache.resetSegments({ k: [{ n: 'mocked-segment-2' }], cn: 150})).toBe(true);

expect(cache.isInSegment('mocked-segment')).toBe(false);
expect(cache.getRegisteredSegments()).toEqual(['mocked-segment-2']);
expect(cache.getKeysCount()).toBe(1);

cache.clear();
expect(cache.getRegisteredSegments()).toEqual([]);
expect(cache.getChangeNumber()).toBe(-1);
});
12 changes: 0 additions & 12 deletions src/storages/inMemory/__tests__/SegmentsCacheInMemory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,4 @@ describe('SEGMENTS CACHE IN MEMORY', () => {
expect(cache.getKeysCount()).toBe(0);
});

test('registerSegment / getRegisteredSegments', async () => {
const cache = new SegmentsCacheInMemory();

await cache.registerSegments(['s1']);
await cache.registerSegments(['s2']);
await cache.registerSegments(['s2', 's3', 's4']);

const segments = cache.getRegisteredSegments();

['s1', 's2', 's3', 's4'].forEach(s => expect(segments.indexOf(s) !== -1).toBe(true));
});

});
6 changes: 6 additions & 0 deletions src/storages/inRedis/RBSegmentsCacheInRedis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync {
);
}

getAll(): Promise<IRBSegment[]> {
return this.getNames().then(names => {
return Promise.all(names.map(name => this.get(name) as Promise<IRBSegment>));
});
}

contains(names: Set<string>): Promise<boolean> {
const namesArray = setToArray(names);
return this.getNames().then(namesInStorage => {
Expand Down
12 changes: 0 additions & 12 deletions src/storages/inRedis/SegmentsCacheInRedis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,6 @@ export class SegmentsCacheInRedis implements ISegmentsCacheAsync {
});
}

registerSegments(segments: string[]) {
if (segments.length) {
return this.redis.sadd(this.keys.buildRegisteredSegmentsKey(), segments).then(() => true);
} else {
return Promise.resolve(true);
}
}

getRegisteredSegments() {
return this.redis.smembers(this.keys.buildRegisteredSegmentsKey());
}

// @TODO remove or implement. It is not being used.
clear() {
return Promise.resolve();
Expand Down
17 changes: 0 additions & 17 deletions src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,4 @@ describe('SEGMENTS CACHE IN REDIS', () => {
await connection.disconnect();
});

test('registerSegment / getRegisteredSegments', async () => {
const connection = new RedisAdapter(loggerMock);
const cache = new SegmentsCacheInRedis(loggerMock, keys, connection);

await cache.registerSegments(['s1']);
await cache.registerSegments(['s2']);
await cache.registerSegments(['s2', 's3', 's4']);

const segments = await cache.getRegisteredSegments();

['s1', 's2', 's3', 's4'].forEach(s => expect(segments.indexOf(s) !== -1).toBe(true));

// Teardown
await connection.del(await connection.keys(`${prefix}.segment*`)); // @TODO use `cache.clear` method when implemented
await connection.disconnect();
});

});
6 changes: 6 additions & 0 deletions src/storages/pluggable/RBSegmentsCachePluggable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export class RBSegmentsCachePluggable implements IRBSegmentsCacheAsync {
);
}

getAll(): Promise<IRBSegment[]> {
return this.getNames().then(names => {
return Promise.all(names.map(name => this.get(name) as Promise<IRBSegment>));
});
}

contains(names: Set<string>): Promise<boolean> {
const namesArray = setToArray(names);
return this.getNames().then(namesInStorage => {
Expand Down
20 changes: 0 additions & 20 deletions src/storages/pluggable/SegmentsCachePluggable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,6 @@ export class SegmentsCachePluggable implements ISegmentsCacheAsync {
});
}

/**
* Add the given segment names to the set of registered segments.
* The returned promise is resolved when the operation success,
* or rejected if it fails (e.g., wrapper operation fails).
*/
registerSegments(segments: string[]) {
if (segments.length) {
return this.wrapper.addItems(this.keys.buildRegisteredSegmentsKey(), segments);
} else {
return Promise.resolve();
}
}

/**
* Returns a promise that resolves with the set of registered segments in a list,
* or rejected if it fails (e.g., wrapper operation fails).
*/
getRegisteredSegments() {
return this.wrapper.getItems(this.keys.buildRegisteredSegmentsKey());
}

// @TODO implement if required by DataLoader or Producer mode
clear(): Promise<boolean> {
Expand Down
12 changes: 0 additions & 12 deletions src/storages/pluggable/__tests__/SegmentsCachePluggable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,4 @@ describe('SEGMENTS CACHE PLUGGABLE', () => {
expect(await cache.isInSegment('inexistent-segment', 'a')).toBe(false);
});

test('registerSegment / getRegisteredSegments', async () => {
const cache = new SegmentsCachePluggable(loggerMock, keyBuilder, wrapperMock);

await cache.registerSegments(['s1']);
await cache.registerSegments(['s2']);
await cache.registerSegments(['s2', 's3', 's4']);

const segments = await cache.getRegisteredSegments();

['s1', 's2', 's3', 's4'].forEach(s => expect(segments.indexOf(s) !== -1).toBe(true));
});

});
13 changes: 6 additions & 7 deletions src/storages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export interface IRBSegmentsCacheBase {
getChangeNumber(): MaybeThenable<number>,
clear(): MaybeThenable<boolean | void>,
contains(names: Set<string>): MaybeThenable<boolean>,
getAll(): MaybeThenable<IRBSegment[]>,
}

export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase {
Expand All @@ -237,6 +238,7 @@ export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase {
getChangeNumber(): number,
clear(): void,
contains(names: Set<string>): boolean,
getAll(): IRBSegment[],
// Used only for smart pausing in client-side standalone. Returns true if the storage contains a RBSegment using segments or large segments matchers
usesSegments(): boolean,
}
Expand All @@ -247,14 +249,13 @@ export interface IRBSegmentsCacheAsync extends IRBSegmentsCacheBase {
getChangeNumber(): Promise<number>,
clear(): Promise<boolean | void>,
contains(names: Set<string>): Promise<boolean>,
getAll(): Promise<IRBSegment[]>,
}

/** Segments cache */

export interface ISegmentsCacheBase {
isInSegment(name: string, key?: string): MaybeThenable<boolean> // different signature on Server and Client-Side
registerSegments(names: string[]): MaybeThenable<boolean | void> // only for Server-Side
getRegisteredSegments(): MaybeThenable<string[]> // only for Server-Side
getChangeNumber(name: string): MaybeThenable<number | undefined> // only for Server-Side
update(name: string, addedKeys: string[], removedKeys: string[], changeNumber: number): MaybeThenable<boolean> // only for Server-Side
clear(): MaybeThenable<boolean | void>
Expand All @@ -263,19 +264,17 @@ export interface ISegmentsCacheBase {
// Same API for both variants: SegmentsCache and MySegmentsCache (client-side API)
export interface ISegmentsCacheSync extends ISegmentsCacheBase {
isInSegment(name: string, key?: string): boolean
registerSegments(names: string[]): boolean
getRegisteredSegments(): string[]
getKeysCount(): number // only used for telemetry
getChangeNumber(name?: string): number | undefined
update(name: string, addedKeys: string[], removedKeys: string[], changeNumber: number): boolean // only for Server-Side
resetSegments(segmentsData: MySegmentsData | IMySegmentsResponse): boolean // only for Sync Client-Side
clear(): void
// only used for telemetry:
getKeysCount(): number
getSegmentsCount(): number
}

export interface ISegmentsCacheAsync extends ISegmentsCacheBase {
isInSegment(name: string, key: string): Promise<boolean>
registerSegments(names: string[]): Promise<boolean | void>
getRegisteredSegments(): Promise<string[]>
getChangeNumber(name: string): Promise<number | undefined>
update(name: string, addedKeys: string[], removedKeys: string[], changeNumber: number): Promise<boolean>
clear(): Promise<boolean | void>
Expand Down
2 changes: 1 addition & 1 deletion src/sync/polling/syncTasks/segmentsSyncTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function segmentsSyncTaskFactory(
segmentChangesUpdaterFactory(
settings.log,
segmentChangesFetcherFactory(fetchSegmentChanges),
storage.segments,
storage,
readiness,
),
settings.scheduler.segmentsRefreshRate,
Expand Down
Loading