@alwaysai/device-agent 2.0.2 → 2.1.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/application-control/config.js +1 -1
- package/lib/application-control/config.js.map +1 -1
- package/lib/application-control/install.d.ts.map +1 -1
- package/lib/application-control/install.js +2 -2
- package/lib/application-control/install.js.map +1 -1
- package/lib/cloud-connection/base-message-handler.d.ts.map +1 -1
- package/lib/cloud-connection/base-message-handler.js +5 -4
- package/lib/cloud-connection/base-message-handler.js.map +1 -1
- package/lib/cloud-connection/bootstrap-agent.d.ts +16 -0
- package/lib/cloud-connection/bootstrap-agent.d.ts.map +1 -0
- package/lib/cloud-connection/{device-agent.js → bootstrap-agent.js} +45 -22
- package/lib/cloud-connection/bootstrap-agent.js.map +1 -0
- package/lib/cloud-connection/connection-manager.d.ts +18 -6
- package/lib/cloud-connection/connection-manager.d.ts.map +1 -1
- package/lib/cloud-connection/connection-manager.js +85 -38
- package/lib/cloud-connection/connection-manager.js.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +13 -12
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/cloud-connection/device-agent-message-handler.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-message-handler.js +19 -18
- package/lib/cloud-connection/device-agent-message-handler.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
- package/lib/cloud-connection/live-updates-handler.js +11 -4
- package/lib/cloud-connection/live-updates-handler.js.map +1 -1
- package/lib/cloud-connection/passthrough-handler.d.ts +3 -3
- package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
- package/lib/cloud-connection/passthrough-handler.js +105 -79
- package/lib/cloud-connection/passthrough-handler.js.map +1 -1
- package/lib/cloud-connection/publisher.d.ts +1 -1
- package/lib/cloud-connection/publisher.d.ts.map +1 -1
- package/lib/cloud-connection/publisher.js +22 -20
- package/lib/cloud-connection/publisher.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.js +3 -3
- package/lib/cloud-connection/shadow-handler.js.map +1 -1
- package/lib/cloud-connection/shadow.d.ts.map +1 -1
- package/lib/cloud-connection/shadow.js +1 -1
- package/lib/cloud-connection/shadow.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
- package/lib/cloud-connection/transaction-manager.js +17 -7
- package/lib/cloud-connection/transaction-manager.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.test.js +52 -44
- package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
- package/lib/device-control/device-control.d.ts.map +1 -1
- package/lib/device-control/device-control.js +13 -9
- package/lib/device-control/device-control.js.map +1 -1
- package/lib/docker/docker-compose.d.ts.map +1 -1
- package/lib/docker/docker-compose.js +1 -1
- package/lib/docker/docker-compose.js.map +1 -1
- package/lib/environment.d.ts.map +1 -1
- package/lib/environment.js +1 -1
- package/lib/environment.js.map +1 -1
- package/lib/index.js +12 -0
- package/lib/index.js.map +1 -1
- package/lib/infrastructure/config-check-utility.js +2 -2
- package/lib/infrastructure/config-check-utility.js.map +1 -1
- package/lib/infrastructure/legacy-migration/legacy-migration.d.ts.map +1 -1
- package/lib/infrastructure/legacy-migration/legacy-migration.js +6 -10
- package/lib/infrastructure/legacy-migration/legacy-migration.js.map +1 -1
- package/lib/local-connection/rabbitmq-container.d.ts +6 -0
- package/lib/local-connection/rabbitmq-container.d.ts.map +1 -0
- package/lib/local-connection/rabbitmq-container.js +111 -0
- package/lib/local-connection/rabbitmq-container.js.map +1 -0
- package/lib/local-connection/rabbitmq-container.test.d.ts +2 -0
- package/lib/local-connection/rabbitmq-container.test.d.ts.map +1 -0
- package/lib/local-connection/rabbitmq-container.test.js +219 -0
- package/lib/local-connection/rabbitmq-container.test.js.map +1 -0
- package/lib/subcommands/device/clean.d.ts.map +1 -1
- package/lib/subcommands/device/clean.js +15 -17
- package/lib/subcommands/device/clean.js.map +1 -1
- package/lib/subcommands/device/index.d.ts.map +1 -1
- package/lib/subcommands/device/index.js +3 -1
- package/lib/subcommands/device/index.js.map +1 -1
- package/lib/subcommands/device/local-connection.d.ts +2 -0
- package/lib/subcommands/device/local-connection.d.ts.map +1 -0
- package/lib/subcommands/device/local-connection.js +17 -0
- package/lib/subcommands/device/local-connection.js.map +1 -0
- package/lib/subcommands/index.d.ts +4 -1
- package/lib/subcommands/index.d.ts.map +1 -1
- package/lib/subcommands/index.js +1 -3
- package/lib/subcommands/index.js.map +1 -1
- package/lib/util/check-for-updates.d.ts.map +1 -1
- package/lib/util/check-for-updates.js +2 -2
- package/lib/util/check-for-updates.js.map +1 -1
- package/lib/util/file.d.ts.map +1 -1
- package/lib/util/file.js +6 -1
- package/lib/util/file.js.map +1 -1
- package/lib/util/file.test.js +1 -1
- package/lib/util/file.test.js.map +1 -1
- package/lib/util/get-device-id.d.ts.map +1 -1
- package/lib/util/get-device-id.js +1 -1
- package/lib/util/get-device-id.js.map +1 -1
- package/package.json +3 -3
- package/src/application-control/config.ts +1 -1
- package/src/application-control/install.ts +3 -2
- package/src/cloud-connection/base-message-handler.ts +10 -5
- package/src/cloud-connection/{device-agent.ts → bootstrap-agent.ts} +68 -35
- package/src/cloud-connection/connection-manager.ts +151 -51
- package/src/cloud-connection/device-agent-cloud-connection.ts +17 -19
- package/src/cloud-connection/device-agent-message-handler.ts +10 -7
- package/src/cloud-connection/live-updates-handler.ts +12 -5
- package/src/cloud-connection/passthrough-handler.ts +137 -92
- package/src/cloud-connection/publisher.ts +30 -28
- package/src/cloud-connection/shadow-handler.ts +3 -3
- package/src/cloud-connection/shadow.ts +3 -1
- package/src/cloud-connection/transaction-manager.test.ts +60 -41
- package/src/cloud-connection/transaction-manager.ts +26 -12
- package/src/device-control/device-control.ts +23 -13
- package/src/docker/docker-compose.ts +3 -1
- package/src/environment.ts +1 -2
- package/src/index.ts +19 -0
- package/src/infrastructure/config-check-utility.ts +2 -2
- package/src/infrastructure/legacy-migration/legacy-migration.ts +8 -13
- package/src/local-connection/rabbitmq-container.test.ts +255 -0
- package/src/local-connection/rabbitmq-container.ts +151 -0
- package/src/subcommands/device/clean.ts +20 -19
- package/src/subcommands/device/index.ts +3 -1
- package/src/subcommands/device/local-connection.ts +16 -0
- package/src/subcommands/index.ts +1 -3
- package/src/util/check-for-updates.ts +4 -2
- package/src/util/file.test.ts +1 -1
- package/src/util/file.ts +7 -1
- package/src/util/get-device-id.ts +3 -1
- package/lib/cloud-connection/bootstrap-provision.d.ts +0 -2
- package/lib/cloud-connection/bootstrap-provision.d.ts.map +0 -1
- package/lib/cloud-connection/bootstrap-provision.js +0 -35
- package/lib/cloud-connection/bootstrap-provision.js.map +0 -1
- package/lib/cloud-connection/device-agent.d.ts +0 -21
- package/lib/cloud-connection/device-agent.d.ts.map +0 -1
- package/lib/cloud-connection/device-agent.js.map +0 -1
- package/lib/local-connection/rabbitmq-connection.d.ts +0 -7
- package/lib/local-connection/rabbitmq-connection.d.ts.map +0 -1
- package/lib/local-connection/rabbitmq-connection.js +0 -95
- package/lib/local-connection/rabbitmq-connection.js.map +0 -1
- package/lib/subcommands/rabbitmq-connection.d.ts +0 -2
- package/lib/subcommands/rabbitmq-connection.d.ts.map +0 -1
- package/lib/subcommands/rabbitmq-connection.js +0 -14
- package/lib/subcommands/rabbitmq-connection.js.map +0 -1
- package/lib/util/clean-certs.d.ts +0 -2
- package/lib/util/clean-certs.d.ts.map +0 -1
- package/lib/util/clean-certs.js +0 -17
- package/lib/util/clean-certs.js.map +0 -1
- package/src/cloud-connection/bootstrap-provision.ts +0 -43
- package/src/local-connection/rabbitmq-connection.ts +0 -124
- package/src/subcommands/rabbitmq-connection.ts +0 -11
- package/src/util/clean-certs.ts +0 -16
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
ModelsInstallResponseMessage
|
|
28
28
|
} from '@alwaysai/device-agent-schemas/lib/app-action-schema';
|
|
29
29
|
import { DeviceActionMessage } from '@alwaysai/device-agent-schemas/lib/device-action-schema';
|
|
30
|
-
import {
|
|
30
|
+
import { stringifyError } from 'alwaysai/lib/util';
|
|
31
31
|
import {
|
|
32
32
|
startApp,
|
|
33
33
|
stopApp,
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
AppConfig,
|
|
46
46
|
validateAppConfig
|
|
47
47
|
} from '@alwaysai/app-configuration-schemas';
|
|
48
|
+
import { logger } from '../util/logger';
|
|
48
49
|
|
|
49
50
|
export type AppContent = {
|
|
50
51
|
projectId: string;
|
|
@@ -160,7 +161,7 @@ class AppStateMessageHandler
|
|
|
160
161
|
});
|
|
161
162
|
} catch (e) {
|
|
162
163
|
logger.error(
|
|
163
|
-
`Error processing application state control request for ${projectId}
|
|
164
|
+
`Error processing application state control request for ${projectId}! Error:\n${stringifyError(
|
|
164
165
|
e
|
|
165
166
|
)}`
|
|
166
167
|
);
|
|
@@ -215,7 +216,7 @@ class AppVersionControlMessageHandler
|
|
|
215
216
|
});
|
|
216
217
|
} catch (e) {
|
|
217
218
|
logger.error(
|
|
218
|
-
`Error processing application install request for ${projectId}
|
|
219
|
+
`Error processing application install request for ${projectId}! Error:\n${stringifyError(
|
|
219
220
|
e
|
|
220
221
|
)}`
|
|
221
222
|
);
|
|
@@ -310,7 +311,9 @@ class AppVersionControlMessageHandler
|
|
|
310
311
|
}
|
|
311
312
|
} catch (e) {
|
|
312
313
|
logger.error(
|
|
313
|
-
`Could not parse the appConfig for transaction
|
|
314
|
+
`Could not parse the appConfig for transaction! Error:\n${stringifyError(
|
|
315
|
+
e
|
|
316
|
+
)}`
|
|
314
317
|
);
|
|
315
318
|
}
|
|
316
319
|
}
|
|
@@ -340,9 +343,9 @@ class AppVersionControlMessageHandler
|
|
|
340
343
|
appContent.envVars = envvarsUpdate;
|
|
341
344
|
}
|
|
342
345
|
} catch (e) {
|
|
343
|
-
// throw here
|
|
346
|
+
// TODO: throw here
|
|
344
347
|
logger.error(
|
|
345
|
-
`Could not parse the environment variables for transaction
|
|
348
|
+
`Could not parse the environment variables for transaction! Error:\n${stringifyError(
|
|
346
349
|
e
|
|
347
350
|
)}`
|
|
348
351
|
);
|
|
@@ -610,7 +613,7 @@ class DeviceActionMessageHandler
|
|
|
610
613
|
logger.error(
|
|
611
614
|
`There was a problem performing device action '${
|
|
612
615
|
message.payload.action
|
|
613
|
-
}'
|
|
616
|
+
}'! Error: \n${stringifyError(e)}`
|
|
614
617
|
);
|
|
615
618
|
this.publisher.publishToClient(
|
|
616
619
|
buildToClientStatusResponseMessage(
|
|
@@ -14,6 +14,9 @@ interface IntervalOptions {
|
|
|
14
14
|
ms?: number;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/*
|
|
18
|
+
Responsible for managing the lifecycle of periodic updates and streams.
|
|
19
|
+
*/
|
|
17
20
|
export class LiveUpdatesHandler {
|
|
18
21
|
private killAllTimeout: ReturnType<typeof setTimeout>;
|
|
19
22
|
private livingIntervals: Record<string, ReturnType<typeof setInterval>> = {};
|
|
@@ -31,12 +34,14 @@ export class LiveUpdatesHandler {
|
|
|
31
34
|
transactionId?: string,
|
|
32
35
|
options?: IntervalOptions
|
|
33
36
|
) {
|
|
37
|
+
logger.silly(`LiveUpdatesHandler: Enabling ${intervalType}`);
|
|
34
38
|
this.restartKillAllTimeout();
|
|
35
39
|
|
|
36
40
|
const key = this.generateIntervalKey(intervalType, transactionId);
|
|
37
41
|
|
|
38
42
|
this.safeSetInterval(key, publishingFn, options);
|
|
39
43
|
|
|
44
|
+
// Call publishing function right away for immediate results
|
|
40
45
|
await publishingFn();
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -44,6 +49,7 @@ export class LiveUpdatesHandler {
|
|
|
44
49
|
intervalType: ToClientMessageTypeValue,
|
|
45
50
|
transactionId?: string
|
|
46
51
|
) {
|
|
52
|
+
logger.silly(`LiveUpdatesHandler: Disabling ${intervalType}`);
|
|
47
53
|
const key = this.generateIntervalKey(intervalType, transactionId);
|
|
48
54
|
clearInterval(this.livingIntervals[key]);
|
|
49
55
|
delete this.livingIntervals[key];
|
|
@@ -54,7 +60,7 @@ export class LiveUpdatesHandler {
|
|
|
54
60
|
streamGetter: () => Promise<NodeJS.ReadableStream | null>,
|
|
55
61
|
publishingFn: (logChunk: string) => void
|
|
56
62
|
) {
|
|
57
|
-
logger.info(`Starting log stream for ${projectId}`);
|
|
63
|
+
logger.info(`LiveUpdatesHandler: Starting log stream for ${projectId}`);
|
|
58
64
|
|
|
59
65
|
this.livingStreams.add(projectId);
|
|
60
66
|
|
|
@@ -84,7 +90,7 @@ export class LiveUpdatesHandler {
|
|
|
84
90
|
});
|
|
85
91
|
|
|
86
92
|
readable.on('finished', () => {
|
|
87
|
-
logger.info(`
|
|
93
|
+
logger.info(`Stream complete. ProjectId: ${projectId}`);
|
|
88
94
|
});
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -105,8 +111,8 @@ export class LiveUpdatesHandler {
|
|
|
105
111
|
try {
|
|
106
112
|
return await streamGetter();
|
|
107
113
|
} catch (e) {
|
|
108
|
-
logger.
|
|
109
|
-
`Failed to start app logs, retrying in 1 second
|
|
114
|
+
logger.error(
|
|
115
|
+
`Failed to start app logs, retrying in 1 second. Error:\n${stringifyError(
|
|
110
116
|
e
|
|
111
117
|
)}`
|
|
112
118
|
);
|
|
@@ -117,7 +123,8 @@ export class LiveUpdatesHandler {
|
|
|
117
123
|
return null;
|
|
118
124
|
}
|
|
119
125
|
|
|
120
|
-
//
|
|
126
|
+
// Do not await any functions in setSafeInterval other than inside
|
|
127
|
+
// setInterval() as per EI-1694.
|
|
121
128
|
private safeSetInterval(
|
|
122
129
|
key: string,
|
|
123
130
|
publishingFn: () => Promise<void>,
|
|
@@ -11,18 +11,21 @@ import {
|
|
|
11
11
|
LOCAL_CONNECTION_ROUTING_KEY
|
|
12
12
|
} from '../local-connection/constants';
|
|
13
13
|
import {
|
|
14
|
-
|
|
14
|
+
runRabbitMQContainer,
|
|
15
15
|
stopRabbitMQContainer
|
|
16
|
-
} from '../local-connection/rabbitmq-
|
|
16
|
+
} from '../local-connection/rabbitmq-container';
|
|
17
17
|
import { logger } from '../util/logger';
|
|
18
18
|
import sleep from '../util/sleep';
|
|
19
19
|
import { Publisher } from './publisher';
|
|
20
20
|
import { ShadowHandler } from './shadow-handler';
|
|
21
21
|
|
|
22
|
-
const messageQueue: any[] = [];
|
|
23
|
-
const ackQueue: any[] = [];
|
|
24
22
|
const MAX_LOCAL_CONNECTION_ATTEMPTS = 10;
|
|
25
23
|
|
|
24
|
+
/*
|
|
25
|
+
Responsible for managing the lifecycle of the Local Connection container
|
|
26
|
+
(RabbitMQ), the connection to it, and the passthrough of messages received
|
|
27
|
+
from the local connection to endpoints of provided `Publisher`.
|
|
28
|
+
*/
|
|
26
29
|
export class PassthroughHandler {
|
|
27
30
|
public publisher: Publisher;
|
|
28
31
|
public shadowHandler: ShadowHandler;
|
|
@@ -35,80 +38,86 @@ export class PassthroughHandler {
|
|
|
35
38
|
this.shadowHandler = shadowHandler;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
logger.debug('Beginning to consume packets');
|
|
41
|
+
private async runLocalConnectionChannel() {
|
|
42
|
+
logger.debug('Beginning to consume packets from Local Connection');
|
|
43
|
+
|
|
40
44
|
await this.channel.consume(
|
|
41
45
|
this.packetQueue,
|
|
42
|
-
(msg) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
async (msg) => {
|
|
47
|
+
if (!msg?.content) return;
|
|
48
|
+
|
|
49
|
+
let packet: string;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const raw = msg.content.toString();
|
|
53
|
+
// Currently, edgeIQ is calling json.dumps twice, so a single JSON.parse
|
|
54
|
+
// results in a string return value, which is needed by publishToCloudWithAck.
|
|
55
|
+
// We will update the edgeIQ implementation in the future.
|
|
56
|
+
packet = JSON.parse(raw);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
logger.error(`Error parsing RabbitMQ packet: ${stringifyError(err)}`);
|
|
59
|
+
this.channel.ack(msg);
|
|
60
|
+
logger.debug('Problematic packet was acknowledged');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// We do a second call to JSON.parse here only to retrieve the 'action' field
|
|
65
|
+
// NOTE: this second call to JSON.parse seems to alter the packet contents
|
|
66
|
+
// (at least when a rounded floating point value is included)
|
|
67
|
+
const action = JSON.parse(packet)?.['action'];
|
|
68
|
+
|
|
69
|
+
switch (action) {
|
|
70
|
+
case 'analytics': {
|
|
50
71
|
try {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// FIXME: put real topic here
|
|
57
|
-
this.publisher.publishToCloudWithAck(
|
|
58
|
-
packet,
|
|
59
|
-
(errOrResp) => {
|
|
60
|
-
while (ackQueue.length > 0) {
|
|
61
|
-
const msg = ackQueue.shift();
|
|
62
|
-
if (errOrResp === true) {
|
|
63
|
-
this.channel.ack(msg); // acknowledge, allow queue to discard
|
|
64
|
-
} else if (errOrResp === false) {
|
|
65
|
-
this.channel.reject(msg, true); // reject and requeue
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
);
|
|
70
|
-
break;
|
|
71
|
-
case 'heartbeat':
|
|
72
|
-
this.channel.ack(msg);
|
|
73
|
-
logger.silly(
|
|
74
|
-
`Heartbeat package received & acknowledged: ${packet}`
|
|
75
|
-
);
|
|
76
|
-
break;
|
|
77
|
-
default:
|
|
78
|
-
this.channel.ack(msg);
|
|
79
|
-
logger.debug(
|
|
80
|
-
`Unknown 'action' package received & acknowledged: ${packet}`
|
|
81
|
-
);
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
72
|
+
const success = await this.publisher.publishToCloudWithAck(
|
|
73
|
+
packet
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (success) {
|
|
85
77
|
this.channel.ack(msg);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
78
|
+
} else {
|
|
79
|
+
this.channel.reject(msg, true); // requeue on failure
|
|
89
80
|
}
|
|
90
|
-
} catch (
|
|
81
|
+
} catch (err) {
|
|
91
82
|
logger.error(
|
|
92
|
-
`
|
|
93
|
-
|
|
94
|
-
|
|
83
|
+
`Error publishing analytics packet: ${stringifyError(err)}`
|
|
84
|
+
);
|
|
85
|
+
this.channel.reject(msg, true);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'heartbeat': {
|
|
91
|
+
this.channel.ack(msg);
|
|
92
|
+
logger.silly(
|
|
93
|
+
`Heartbeat packet received & acknowledged:\n${packet}`
|
|
94
|
+
);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
default: {
|
|
99
|
+
this.channel.ack(msg);
|
|
100
|
+
if (action) {
|
|
101
|
+
logger.debug(
|
|
102
|
+
`Unknown 'action' packet received & acknowledged:\n${packet}`
|
|
103
|
+
);
|
|
104
|
+
} else {
|
|
105
|
+
logger.debug(
|
|
106
|
+
`Received & acknowledged a RabbitMQ packet of unknown structure:\n${packet}`
|
|
95
107
|
);
|
|
96
|
-
this.channel.ack(msg);
|
|
97
|
-
logger.debug('Problematic packet was acknowledged');
|
|
98
108
|
}
|
|
109
|
+
break;
|
|
99
110
|
}
|
|
100
111
|
}
|
|
101
112
|
},
|
|
102
|
-
{
|
|
103
|
-
noAck: false // When true, RabbitMQ deletes message as soon as it is consumed
|
|
104
|
-
}
|
|
113
|
+
{ noAck: false } // message must be explicitly acked/rejected
|
|
105
114
|
);
|
|
106
|
-
}
|
|
115
|
+
}
|
|
107
116
|
|
|
108
|
-
async establishLocalConnection(): Promise<void> {
|
|
117
|
+
private async establishLocalConnection(): Promise<void> {
|
|
109
118
|
let connectAttempts = 0;
|
|
110
119
|
let connected = false;
|
|
111
|
-
logger.debug(`Establishing
|
|
120
|
+
logger.debug(`Establishing Local Connection...`);
|
|
112
121
|
while (
|
|
113
122
|
connectAttempts <= MAX_LOCAL_CONNECTION_ATTEMPTS &&
|
|
114
123
|
this.connection === undefined
|
|
@@ -117,14 +126,40 @@ export class PassthroughHandler {
|
|
|
117
126
|
this.connection = await amqp.connect(
|
|
118
127
|
`amqp://${LOCAL_CONNECTION_HOST}:${LOCAL_CONNECTION_PORT}`
|
|
119
128
|
);
|
|
120
|
-
|
|
121
|
-
this.connection.on('error', async () => {
|
|
122
|
-
logger.error(
|
|
123
|
-
|
|
129
|
+
|
|
130
|
+
this.connection.on('error', async (e) => {
|
|
131
|
+
logger.error(
|
|
132
|
+
`Local Connection failed due to ${stringifyError(
|
|
133
|
+
e
|
|
134
|
+
)}. Reconnect will be handled by "close"`
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.connection.on('close', async () => {
|
|
139
|
+
// Close should be called for every connection loss, error or not.
|
|
140
|
+
logger.warn(
|
|
141
|
+
`Local Connection closed by server. Attempting to reconnect...`
|
|
142
|
+
);
|
|
143
|
+
const stopped = await stopRabbitMQContainer();
|
|
144
|
+
if (stopped === false) {
|
|
145
|
+
logger.warn(
|
|
146
|
+
'Failed to stop Local Connection container. Restart may fail...'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
124
149
|
this.connection = undefined;
|
|
125
|
-
await this.
|
|
150
|
+
await this.run();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.connection.on('blocked', async () => {
|
|
154
|
+
logger.warn(`Local Connection blocked.`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.connection.on('unblocked', async () => {
|
|
158
|
+
logger.warn(`Local Connection unblocked.`);
|
|
126
159
|
});
|
|
127
160
|
|
|
161
|
+
this.channel = await this.connection.createChannel();
|
|
162
|
+
|
|
128
163
|
connected = true;
|
|
129
164
|
} catch (e) {
|
|
130
165
|
const timeTillNextAttemptMs = 1000 + 1000 * connectAttempts;
|
|
@@ -143,7 +178,7 @@ export class PassthroughHandler {
|
|
|
143
178
|
await this.channel.assertQueue(this.packetQueue, {
|
|
144
179
|
durable: true
|
|
145
180
|
});
|
|
146
|
-
logger.info(`Local
|
|
181
|
+
logger.info(`Local Connection established.`);
|
|
147
182
|
} else {
|
|
148
183
|
throw new Error(
|
|
149
184
|
'Unable to establish connection to alwaysAI Local Connection, please try restarting Device Agent.'
|
|
@@ -166,30 +201,40 @@ export class PassthroughHandler {
|
|
|
166
201
|
);
|
|
167
202
|
}
|
|
168
203
|
|
|
169
|
-
async
|
|
170
|
-
if (ALWAYSAI_ANALYTICS_PASSTHROUGH ===
|
|
171
|
-
logger.debug(
|
|
172
|
-
`Setting up alwaysAI Local Connection on host: ${LOCAL_CONNECTION_HOST} and channel key: ${LOCAL_CONNECTION_ROUTING_KEY}`
|
|
173
|
-
);
|
|
174
|
-
await this.publishPassthroughStatusUpdate('starting');
|
|
175
|
-
await setupRabbitMQContainer();
|
|
176
|
-
try {
|
|
177
|
-
await this.establishLocalConnection();
|
|
178
|
-
await this.runChannel();
|
|
179
|
-
await this.publishPassthroughStatusUpdate(
|
|
180
|
-
'running',
|
|
181
|
-
`Passthrough running on host: ${LOCAL_CONNECTION_HOST} and channel key: ${LOCAL_CONNECTION_ROUTING_KEY}`
|
|
182
|
-
);
|
|
183
|
-
} catch (e) {
|
|
184
|
-
logger.error(
|
|
185
|
-
`There was a problem maintaining RabbitMQ connection!\n${stringifyError(
|
|
186
|
-
e
|
|
187
|
-
)}`
|
|
188
|
-
);
|
|
189
|
-
await this.publishPassthroughStatusUpdate('error', stringifyError(e));
|
|
190
|
-
}
|
|
191
|
-
} else {
|
|
204
|
+
async run() {
|
|
205
|
+
if (ALWAYSAI_ANALYTICS_PASSTHROUGH === false) {
|
|
206
|
+
logger.debug(`alwaysAI Local Connection disabled`);
|
|
192
207
|
await this.publishPassthroughStatusUpdate('disabled');
|
|
208
|
+
return;
|
|
193
209
|
}
|
|
210
|
+
|
|
211
|
+
logger.debug(
|
|
212
|
+
`Setting up alwaysAI Local Connection on host: ${LOCAL_CONNECTION_HOST} and channel key: ${LOCAL_CONNECTION_ROUTING_KEY}`
|
|
213
|
+
);
|
|
214
|
+
await this.publishPassthroughStatusUpdate('starting');
|
|
215
|
+
const containerUp = await runRabbitMQContainer();
|
|
216
|
+
if (!containerUp) {
|
|
217
|
+
await this.publishPassthroughStatusUpdate(
|
|
218
|
+
'error',
|
|
219
|
+
'Local Connection container failed to start!'
|
|
220
|
+
);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
await this.establishLocalConnection();
|
|
225
|
+
await this.runLocalConnectionChannel();
|
|
226
|
+
} catch (e) {
|
|
227
|
+
logger.error(
|
|
228
|
+
`There was a problem maintaining RabbitMQ connection! Error:\n${stringifyError(
|
|
229
|
+
e
|
|
230
|
+
)}`
|
|
231
|
+
);
|
|
232
|
+
await this.publishPassthroughStatusUpdate('error', stringifyError(e));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
await this.publishPassthroughStatusUpdate(
|
|
236
|
+
'running',
|
|
237
|
+
`Passthrough running on host: ${LOCAL_CONNECTION_HOST} and channel key: ${LOCAL_CONNECTION_ROUTING_KEY}`
|
|
238
|
+
);
|
|
194
239
|
}
|
|
195
240
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '@alwaysai/device-agent-schemas';
|
|
8
8
|
import * as winston from 'winston';
|
|
9
9
|
import { ConnectionManager } from './connection-manager';
|
|
10
|
+
import { mqtt5 } from 'aws-iot-device-sdk-v2';
|
|
10
11
|
|
|
11
12
|
export class Publisher {
|
|
12
13
|
private connectionManager: ConnectionManager;
|
|
@@ -28,6 +29,12 @@ export class Publisher {
|
|
|
28
29
|
) {
|
|
29
30
|
// TODO: topic validation
|
|
30
31
|
// By default, log the published message at debug level, unless otherwise specified
|
|
32
|
+
const publishPacket: mqtt5.PublishPacket = {
|
|
33
|
+
topicName: topic,
|
|
34
|
+
qos: this.connectionManager.qos,
|
|
35
|
+
payload: Buffer.from(payload)
|
|
36
|
+
};
|
|
37
|
+
|
|
31
38
|
customLogger(
|
|
32
39
|
`Publishing message:\nTopic: ${topic}\nMessage: ${JSON.stringify(
|
|
33
40
|
JSON.parse(payload),
|
|
@@ -35,37 +42,32 @@ export class Publisher {
|
|
|
35
42
|
2
|
|
36
43
|
)}`
|
|
37
44
|
);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (err) {
|
|
42
|
-
logger.error(
|
|
43
|
-
`Error publishing message: \nTopic: ${topic}\nMessage: ${payload}\nError: ${err}`
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
45
|
+
|
|
46
|
+
// TODO: Change publish() to async and resolve issues where it's used synchronously.
|
|
47
|
+
void this.connectionManager.getIoTDevice().publish(publishPacket);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
public publishToCloudWithAck(
|
|
50
|
-
payload: string,
|
|
51
|
-
ackNackCallback: CallableFunction
|
|
52
|
-
) {
|
|
50
|
+
public async publishToCloudWithAck(payload: string): Promise<boolean> {
|
|
53
51
|
const topic = this.toCloudTopic;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
52
|
+
|
|
53
|
+
const publishPacket: mqtt5.PublishPacket = {
|
|
54
|
+
topicName: topic,
|
|
55
|
+
qos: this.connectionManager.qos,
|
|
56
|
+
payload: Buffer.from(payload)
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await this.connectionManager.getIoTDevice().publish(publishPacket);
|
|
61
|
+
logger.debug(
|
|
62
|
+
`Successfully published message:\nTopic: ${topic}\nMessage: ${payload}`
|
|
63
|
+
);
|
|
64
|
+
return true;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
logger.error(
|
|
67
|
+
`Error publishing message:\nTopic: ${topic}\nMessage: ${payload}\nError: ${err}`
|
|
68
|
+
);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
public publishDeviceAgentMessage(
|
|
@@ -125,7 +125,7 @@ export class ShadowHandler {
|
|
|
125
125
|
newAppCfg = JSON.parse(appConfig);
|
|
126
126
|
} catch (e) {
|
|
127
127
|
logger.error(
|
|
128
|
-
`Could not parse the appConfig for transaction ${txId}
|
|
128
|
+
`Could not parse the appConfig for transaction ${txId}! Error:\n${stringifyError(
|
|
129
129
|
e
|
|
130
130
|
)}`
|
|
131
131
|
);
|
|
@@ -172,7 +172,7 @@ export class ShadowHandler {
|
|
|
172
172
|
newEnvVars = JSON.parse(envVars);
|
|
173
173
|
} catch (e) {
|
|
174
174
|
logger.error(
|
|
175
|
-
`Could not parse the environment variables for transaction ${txId}
|
|
175
|
+
`Could not parse the environment variables for transaction ${txId}! Error:\n${stringifyError(
|
|
176
176
|
e
|
|
177
177
|
)}`
|
|
178
178
|
);
|
|
@@ -493,7 +493,7 @@ export class ProjectShadowMessageHandler
|
|
|
493
493
|
})
|
|
494
494
|
.catch((e) => {
|
|
495
495
|
logger.error(
|
|
496
|
-
`There was an issue updating project shadow config for ${projectId}
|
|
496
|
+
`There was an issue updating project shadow config for ${projectId}! Error:\n${stringifyError(
|
|
497
497
|
e
|
|
498
498
|
)}`
|
|
499
499
|
);
|
|
@@ -45,7 +45,9 @@ export const getAppCfgModelsDiff = async ({
|
|
|
45
45
|
});
|
|
46
46
|
} catch (e) {
|
|
47
47
|
logger.error(
|
|
48
|
-
`Error parsing app config update for ${projectId}
|
|
48
|
+
`Error parsing app config update for ${projectId}! Error:\n${stringifyError(
|
|
49
|
+
e
|
|
50
|
+
)}`
|
|
49
51
|
);
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -115,78 +115,97 @@ describe('Test Transaction Manager', () => {
|
|
|
115
115
|
test('Attempt to start an ongoing transaction, results in failure', async () => {
|
|
116
116
|
const txId = generateTxId();
|
|
117
117
|
const projectId = generateRandomProjectId();
|
|
118
|
+
let error;
|
|
119
|
+
const errorFn = jest.fn().mockImplementation((txid, message) => {
|
|
120
|
+
error = message;
|
|
121
|
+
});
|
|
118
122
|
await txnMgr.runTransactionStep({
|
|
119
123
|
func: func_incomplete,
|
|
120
124
|
projectId,
|
|
121
125
|
txId,
|
|
122
126
|
start: true,
|
|
123
|
-
stepName: 'step1'
|
|
127
|
+
stepName: 'step1',
|
|
128
|
+
errorFn
|
|
129
|
+
});
|
|
130
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
131
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
132
|
+
expect(errorFn).toBeCalledTimes(0);
|
|
133
|
+
|
|
134
|
+
await txnMgr.runTransactionStep({
|
|
135
|
+
func: func_incomplete,
|
|
136
|
+
projectId,
|
|
137
|
+
txId,
|
|
138
|
+
start: true,
|
|
139
|
+
stepName: 'step2',
|
|
140
|
+
errorFn
|
|
124
141
|
});
|
|
125
|
-
try {
|
|
126
|
-
await txnMgr.runTransactionStep({
|
|
127
|
-
func: func_incomplete,
|
|
128
|
-
projectId,
|
|
129
|
-
txId,
|
|
130
|
-
start: true,
|
|
131
|
-
stepName: 'step2'
|
|
132
|
-
});
|
|
133
|
-
throw new Error('Expected starting transaction to fail!');
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.log(e);
|
|
136
|
-
expect(e.code).toBe(txnMgr.Errors.TRANSACTION_ONGOING);
|
|
137
|
-
}
|
|
138
142
|
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
139
143
|
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
144
|
+
expect(errorFn).toBeCalledTimes(1);
|
|
145
|
+
expect(error.includes(`Transaction ${txId} already ongoing`)).toBeTruthy();
|
|
140
146
|
});
|
|
141
147
|
|
|
142
148
|
test('Attempt to continue a transaction that is not ongoing', async () => {
|
|
143
149
|
const txId = generateTxId();
|
|
144
150
|
const projectId = generateRandomProjectId();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
151
|
+
let error;
|
|
152
|
+
const errorFn = jest.fn().mockImplementation((txid, message) => {
|
|
153
|
+
error = message;
|
|
154
|
+
});
|
|
155
|
+
await txnMgr.runTransactionStep({
|
|
156
|
+
func: func_incomplete,
|
|
157
|
+
projectId,
|
|
158
|
+
txId,
|
|
159
|
+
start: false,
|
|
160
|
+
stepName: 'step1',
|
|
161
|
+
errorFn
|
|
162
|
+
});
|
|
158
163
|
expect(txnMgr.isOngoingTransaction(txId)).toBe(false);
|
|
159
164
|
expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
|
|
160
165
|
expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
|
|
166
|
+
expect(errorFn).toBeCalledTimes(1);
|
|
167
|
+
expect(
|
|
168
|
+
error.includes(
|
|
169
|
+
`Cannot update transaction ${txId} since it doesn't exist!`
|
|
170
|
+
)
|
|
171
|
+
).toBeTruthy();
|
|
161
172
|
});
|
|
162
173
|
|
|
163
174
|
test('Attempt to start a transaction for a project with an ongoing transaction, results in failure', async () => {
|
|
164
175
|
const txId = generateTxId();
|
|
165
176
|
const txId2 = generateTxId();
|
|
166
177
|
const projectId = generateRandomProjectId();
|
|
178
|
+
let error;
|
|
179
|
+
const errorFn = jest.fn().mockImplementation((txid, message) => {
|
|
180
|
+
error = message;
|
|
181
|
+
});
|
|
167
182
|
await txnMgr.runTransactionStep({
|
|
168
183
|
func: func_incomplete,
|
|
169
184
|
projectId,
|
|
170
185
|
txId,
|
|
171
186
|
start: true,
|
|
172
|
-
stepName: 'step1'
|
|
187
|
+
stepName: 'step1',
|
|
188
|
+
errorFn
|
|
189
|
+
});
|
|
190
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
191
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
192
|
+
expect(errorFn).toBeCalledTimes(0);
|
|
193
|
+
|
|
194
|
+
await txnMgr.runTransactionStep({
|
|
195
|
+
func: func_incomplete,
|
|
196
|
+
projectId,
|
|
197
|
+
txId: txId2,
|
|
198
|
+
start: true,
|
|
199
|
+
stepName: 'step2',
|
|
200
|
+
errorFn
|
|
173
201
|
});
|
|
174
|
-
try {
|
|
175
|
-
await txnMgr.runTransactionStep({
|
|
176
|
-
func: func_incomplete,
|
|
177
|
-
projectId,
|
|
178
|
-
txId: txId2,
|
|
179
|
-
start: true,
|
|
180
|
-
stepName: 'step2'
|
|
181
|
-
});
|
|
182
|
-
throw new Error('Expected start transaction to fail!');
|
|
183
|
-
} catch (e) {
|
|
184
|
-
console.log(e);
|
|
185
|
-
expect(e.code).toBe(txnMgr.Errors.PROJECT_TRANSACTION_ONGOING);
|
|
186
|
-
}
|
|
187
202
|
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
188
203
|
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
189
204
|
expect(txnMgr.getProjectFromTransaction(txId2)).toBeUndefined();
|
|
205
|
+
expect(errorFn).toBeCalledTimes(1);
|
|
206
|
+
expect(
|
|
207
|
+
error.includes(`Project ${projectId} already has an ongoing transaction`)
|
|
208
|
+
).toBeTruthy();
|
|
190
209
|
});
|
|
191
210
|
|
|
192
211
|
test('Handle error in step function', async () => {
|