Skip to content

Commit 90ea0fe

Browse files
aabmassdyladanobecny
authored
fix(@opentelemetry/exporter-collector): remove fulfilled promises cor… (#1775)
Co-authored-by: Daniel Dyla <[email protected]> Co-authored-by: Bartlomiej Obecny <[email protected]>
1 parent 4553b29 commit 90ea0fe

File tree

6 files changed

+202
-71
lines changed

6 files changed

+202
-71
lines changed

packages/opentelemetry-exporter-collector-grpc/src/CollectorExporterNodeBase.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,17 @@ export abstract class CollectorExporterNodeBase<
6161
onSuccess: () => void,
6262
onError: (error: collectorTypes.CollectorExporterError) => void
6363
): void {
64-
const promise = new Promise<void>(resolve => {
65-
const _onSuccess = (): void => {
66-
onSuccess();
67-
_onFinish();
68-
};
69-
const _onError = (error: collectorTypes.CollectorExporterError): void => {
70-
onError(error);
71-
_onFinish();
72-
};
73-
const _onFinish = () => {
74-
resolve();
75-
const index = this._sendingPromises.indexOf(promise);
76-
this._sendingPromises.splice(index, 1);
77-
};
78-
79-
this._send(this, objects, _onSuccess, _onError);
80-
});
64+
const promise = new Promise<void>((resolve, reject) => {
65+
this._send(this, objects, resolve, reject);
66+
})
67+
.then(onSuccess, onError);
8168

8269
this._sendingPromises.push(promise);
70+
const popPromise = () => {
71+
const index = this._sendingPromises.indexOf(promise);
72+
this._sendingPromises.splice(index, 1);
73+
}
74+
promise.then(popPromise, popPromise);
8375
}
8476

8577
onInit(config: CollectorExporterConfigNode): void {
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { collectorTypes } from '@opentelemetry/exporter-collector';
18+
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
19+
20+
import * as assert from 'assert';
21+
import { CollectorExporterNodeBase } from '../src/CollectorExporterNodeBase';
22+
import { CollectorExporterConfigNode, ServiceClientType } from '../src/types';
23+
import { mockedReadableSpan } from './helper';
24+
25+
class MockCollectorExporter extends CollectorExporterNodeBase<
26+
ReadableSpan,
27+
ReadableSpan[]
28+
> {
29+
/**
30+
* Callbacks passed to _send()
31+
*/
32+
sendCallbacks: {
33+
onSuccess: () => void;
34+
onError: (error: collectorTypes.CollectorExporterError) => void;
35+
}[] = [];
36+
37+
getDefaultUrl(config: CollectorExporterConfigNode): string {
38+
return '';
39+
}
40+
41+
getDefaultServiceName(config: CollectorExporterConfigNode): string {
42+
return '';
43+
}
44+
45+
convert(spans: ReadableSpan[]): ReadableSpan[] {
46+
return spans;
47+
}
48+
49+
getServiceClientType() {
50+
return ServiceClientType.SPANS;
51+
}
52+
53+
getServiceProtoPath(): string {
54+
return 'opentelemetry/proto/collector/trace/v1/trace_service.proto';
55+
}
56+
}
57+
58+
// Mocked _send which just saves the callbacks for later
59+
MockCollectorExporter.prototype['_send'] = function _sendMock(
60+
self: MockCollectorExporter,
61+
objects: ReadableSpan[],
62+
onSuccess: () => void,
63+
onError: (error: collectorTypes.CollectorExporterError) => void
64+
): void {
65+
self.sendCallbacks.push({ onSuccess, onError });
66+
};
67+
68+
describe('CollectorExporterNodeBase', () => {
69+
let exporter: MockCollectorExporter;
70+
const concurrencyLimit = 5;
71+
72+
beforeEach(done => {
73+
exporter = new MockCollectorExporter({ concurrencyLimit });
74+
done();
75+
});
76+
77+
describe('export', () => {
78+
it('should export requests concurrently', async () => {
79+
const spans = [Object.assign({}, mockedReadableSpan)];
80+
const numToExport = concurrencyLimit;
81+
82+
for (let i = 0; i < numToExport; ++i) {
83+
exporter.export(spans, () => {});
84+
}
85+
86+
assert.strictEqual(exporter['_sendingPromises'].length, numToExport);
87+
const promisesAllDone = Promise.all(exporter['_sendingPromises']);
88+
// Mock that all requests finish sending
89+
exporter.sendCallbacks.forEach(({ onSuccess }) => onSuccess());
90+
91+
// All finished promises should be popped off
92+
await promisesAllDone;
93+
assert.strictEqual(exporter['_sendingPromises'].length, 0);
94+
});
95+
96+
it('should drop new export requests when already sending at concurrencyLimit', async () => {
97+
const spans = [Object.assign({}, mockedReadableSpan)];
98+
const numToExport = concurrencyLimit + 5;
99+
100+
for (let i = 0; i < numToExport; ++i) {
101+
exporter.export(spans, () => {});
102+
}
103+
104+
assert.strictEqual(exporter['_sendingPromises'].length, concurrencyLimit);
105+
const promisesAllDone = Promise.all(exporter['_sendingPromises']);
106+
// Mock that all requests finish sending
107+
exporter.sendCallbacks.forEach(({ onSuccess }) => onSuccess());
108+
109+
// All finished promises should be popped off
110+
await promisesAllDone;
111+
assert.strictEqual(exporter['_sendingPromises'].length, 0);
112+
});
113+
114+
it('should pop export request promises even if they failed', async () => {
115+
const spans = [Object.assign({}, mockedReadableSpan)];
116+
117+
exporter.export(spans, () => {});
118+
assert.strictEqual(exporter['_sendingPromises'].length, 1);
119+
const promisesAllDone = Promise.all(exporter['_sendingPromises']);
120+
// Mock that all requests fail sending
121+
exporter.sendCallbacks.forEach(({ onError }) =>
122+
onError(new Error('Failed to send!!'))
123+
);
124+
125+
// All finished promises should be popped off
126+
await promisesAllDone;
127+
assert.strictEqual(exporter['_sendingPromises'].length, 0);
128+
});
129+
130+
it('should pop export request promises even if success callback throws error', async () => {
131+
const spans = [Object.assign({}, mockedReadableSpan)];
132+
133+
exporter['_sendPromise'](
134+
spans,
135+
() => {
136+
throw new Error('Oops');
137+
},
138+
() => {}
139+
);
140+
141+
assert.strictEqual(exporter['_sendingPromises'].length, 1);
142+
const promisesAllDone = Promise.all(exporter['_sendingPromises'])
143+
// catch expected unhandled exception
144+
.catch(() => {});
145+
146+
// Mock that the request finishes sending
147+
exporter.sendCallbacks.forEach(({ onSuccess }) => {
148+
onSuccess();
149+
});
150+
151+
// All finished promises should be popped off
152+
await promisesAllDone;
153+
assert.strictEqual(exporter['_sendingPromises'].length, 0);
154+
});
155+
});
156+
});

packages/opentelemetry-exporter-collector-proto/src/CollectorExporterNodeBase.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,17 @@ export abstract class CollectorExporterNodeBase<
4747
onSuccess: () => void,
4848
onError: (error: collectorTypes.CollectorExporterError) => void
4949
): void {
50-
const promise = new Promise<void>(resolve => {
51-
const _onSuccess = (): void => {
52-
onSuccess();
53-
_onFinish();
54-
};
55-
const _onError = (error: collectorTypes.CollectorExporterError): void => {
56-
onError(error);
57-
_onFinish();
58-
};
59-
const _onFinish = () => {
60-
resolve();
61-
const index = this._sendingPromises.indexOf(promise);
62-
this._sendingPromises.splice(index, 1);
63-
};
64-
65-
this._send(this, objects, this.compression, _onSuccess, _onError);
66-
});
50+
const promise = new Promise<void>((resolve, reject) => {
51+
this._send(this, objects, this.compression, resolve, reject);
52+
})
53+
.then(onSuccess, onError);
6754

6855
this._sendingPromises.push(promise);
56+
const popPromise = () => {
57+
const index = this._sendingPromises.indexOf(promise);
58+
this._sendingPromises.splice(index, 1);
59+
}
60+
promise.then(popPromise, popPromise);
6961
}
7062

7163
override onInit(config: CollectorExporterNodeConfigBase): void {

packages/opentelemetry-exporter-collector/src/platform/browser/CollectorExporterBrowserBase.ts

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -76,27 +76,20 @@ export abstract class CollectorExporterBrowserBase<
7676
const serviceRequest = this.convert(items);
7777
const body = JSON.stringify(serviceRequest);
7878

79-
const promise = new Promise<void>(resolve => {
80-
const _onSuccess = (): void => {
81-
onSuccess();
82-
_onFinish();
83-
};
84-
const _onError = (error: collectorTypes.CollectorExporterError): void => {
85-
onError(error);
86-
_onFinish();
87-
};
88-
const _onFinish = () => {
89-
resolve();
90-
const index = this._sendingPromises.indexOf(promise);
91-
this._sendingPromises.splice(index, 1);
92-
};
93-
79+
const promise = new Promise<void>((resolve, reject) => {
9480
if (this._useXHR) {
95-
sendWithXhr(body, this.url, this._headers, _onSuccess, _onError);
81+
sendWithXhr(body, this.url, this._headers, resolve, reject);
9682
} else {
97-
sendWithBeacon(body, this.url, { type: 'application/json' }, _onSuccess, _onError);
83+
sendWithBeacon(body, this.url, { type: 'application/json' }, resolve, reject);
9884
}
99-
});
85+
})
86+
.then(onSuccess, onError);
87+
10088
this._sendingPromises.push(promise);
89+
const popPromise = () => {
90+
const index = this._sendingPromises.indexOf(promise);
91+
this._sendingPromises.splice(index, 1);
92+
}
93+
promise.then(popPromise, popPromise);
10194
}
10295
}

packages/opentelemetry-exporter-collector/src/platform/node/CollectorExporterNodeBase.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,30 +70,23 @@ export abstract class CollectorExporterNodeBase<
7070
}
7171
const serviceRequest = this.convert(objects);
7272

73-
const promise = new Promise<void>(resolve => {
74-
const _onSuccess = (): void => {
75-
onSuccess();
76-
_onFinish();
77-
};
78-
const _onError = (error: collectorTypes.CollectorExporterError): void => {
79-
onError(error);
80-
_onFinish();
81-
};
82-
const _onFinish = () => {
83-
resolve();
84-
const index = this._sendingPromises.indexOf(promise);
85-
this._sendingPromises.splice(index, 1);
86-
};
73+
const promise = new Promise<void>((resolve, reject) => {
8774
sendWithHttp(
8875
this,
8976
JSON.stringify(serviceRequest),
9077
'application/json',
91-
_onSuccess,
92-
_onError
78+
resolve,
79+
reject
9380
);
94-
});
81+
})
82+
.then(onSuccess, onError);
9583

9684
this._sendingPromises.push(promise);
85+
const popPromise = () => {
86+
const index = this._sendingPromises.indexOf(promise);
87+
this._sendingPromises.splice(index, 1);
88+
}
89+
promise.then(popPromise, popPromise);
9790
}
9891

9992
onShutdown(): void {}

packages/opentelemetry-exporter-zipkin/src/zipkin.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,16 @@ export class ZipkinExporter implements SpanExporter {
8484
this._sendSpans(spans, serviceName, result => {
8585
resolve();
8686
resultCallback(result);
87-
const index = this._sendingPromises.indexOf(promise);
88-
this._sendingPromises.splice(index, 1);
8987
});
9088
});
89+
90+
9191
this._sendingPromises.push(promise);
92+
const popPromise = () => {
93+
const index = this._sendingPromises.indexOf(promise);
94+
this._sendingPromises.splice(index, 1);
95+
}
96+
promise.then(popPromise, popPromise);
9297
}
9398

9499
/**

0 commit comments

Comments
 (0)