Skip to content

Commit

Permalink
feat: add sensors for cluster state
Browse files Browse the repository at this point in the history
Makes it easier to debug and be notified on issues with the
room-assistant cluster.

Closes #127
  • Loading branch information
mKeRix committed May 2, 2020
1 parent 8c88369 commit b8249e8
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/app.module.ts
Expand Up @@ -9,6 +9,7 @@ import _ from 'lodash';
import { NestEmitterModule } from 'nest-emitter';
import { EventEmitter } from 'events';
import { WINSTON_LOGGER } from './logger';
import { StatusModule } from './status/status.module';

export const VERSION = require('../package.json').version;
export const CONFIGURED_INTEGRATIONS = c
Expand All @@ -21,6 +22,7 @@ export const CONFIGURED_INTEGRATIONS = c
EntitiesModule,
ConfigModule,
ClusterModule,
StatusModule,
ScheduleModule.forRoot(),
NestEmitterModule.forRoot(new EventEmitter()),
IntegrationsModule.register(CONFIGURED_INTEGRATIONS, WINSTON_LOGGER)
Expand Down
11 changes: 11 additions & 0 deletions src/status/status.module.ts
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { StatusService } from './status.service';
import { ClusterModule } from '../cluster/cluster.module';
import { EntitiesModule } from '../entities/entities.module';
import { ConfigModule } from '../config/config.module';

@Module({
imports: [ClusterModule, EntitiesModule, ConfigModule],
providers: [StatusService]
})
export class StatusModule {}
129 changes: 129 additions & 0 deletions src/status/status.service.spec.ts
@@ -0,0 +1,129 @@
import { Test, TestingModule } from '@nestjs/testing';
import { StatusService } from './status.service';
import { ConfigModule } from '../config/config.module';
import { EntitiesModule } from '../entities/entities.module';
import { ClusterModule } from '../cluster/cluster.module';
import { EntitiesService } from '../entities/entities.service';
import { ClusterService } from '../cluster/cluster.service';
import { Sensor } from '../entities/sensor';

describe('StatusService', () => {
let service: StatusService;
const entitiesService = {
add: jest.fn()
};
const clusterService = {
on: jest.fn(),
nodes: jest.fn(),
leader: jest.fn(),
quorumReached: jest.fn()
};

beforeEach(async () => {
jest.clearAllMocks();

const module: TestingModule = await Test.createTestingModule({
imports: [ConfigModule, EntitiesModule, ClusterModule],
providers: [StatusService]
})
.overrideProvider(EntitiesService)
.useValue(entitiesService)
.overrideProvider(ClusterService)
.useValue(clusterService)
.compile();

service = module.get<StatusService>(StatusService);
});

it('should register new sensors on bootstrap', () => {
jest
.spyOn(service, 'updateClusterSizeSensor')
.mockImplementation(() => undefined);
jest
.spyOn(service, 'updateClusterLeaderSensor')
.mockImplementation(() => undefined);

service.onApplicationBootstrap();

expect(entitiesService.add).toHaveBeenCalledTimes(2);
expect(entitiesService.add).toHaveBeenCalledWith(
new Sensor('status-cluster-size', 'test-instance Cluster Size'),
expect.any(Array)
);
expect(entitiesService.add).toHaveBeenCalledWith(
new Sensor('status-cluster-leader', 'test-instance Cluster Leader'),
expect.any(Array)
);
});

it('should update sensors on bootstrap', () => {
const sizeSpy = jest
.spyOn(service, 'updateClusterSizeSensor')
.mockImplementation(() => undefined);
const leaderSpy = jest
.spyOn(service, 'updateClusterLeaderSensor')
.mockImplementation(() => undefined);

service.onApplicationBootstrap();

expect(sizeSpy).toHaveBeenCalledTimes(1);
expect(leaderSpy).toHaveBeenCalledTimes(1);
});

it('should subscribe to the cluster service events on boostrap', () => {
jest
.spyOn(service, 'updateClusterSizeSensor')
.mockImplementation(() => undefined);
jest
.spyOn(service, 'updateClusterLeaderSensor')
.mockImplementation(() => undefined);

service.onApplicationBootstrap();

expect(clusterService.on).toHaveBeenCalledTimes(3);
});

it('should update the cluster size sensor', () => {
const sensor = new Sensor('cluster-size', 'Cluster Size');

jest
.spyOn(service, 'updateClusterLeaderSensor')
.mockImplementation(() => undefined);
entitiesService.add.mockReturnValue(sensor);
clusterService.nodes.mockReturnValue({});
service.onApplicationBootstrap();

clusterService.nodes.mockReturnValue({
node1: {},
node2: {}
});

service.updateClusterSizeSensor();

expect(sensor.state).toBe(2);
expect(sensor.attributes.nodes).toStrictEqual(['node1', 'node2']);
});

it('should update the cluster leader sensor', () => {
const sensor = new Sensor('cluster-leader', 'Cluster Leader');

jest
.spyOn(service, 'updateClusterSizeSensor')
.mockImplementation(() => undefined);
entitiesService.add.mockReturnValue(sensor);
clusterService.leader.mockReturnValue({
id: 'node1'
});
service.onApplicationBootstrap();

clusterService.leader.mockReturnValue({
id: 'node2'
});
clusterService.quorumReached.mockReturnValue(true);

service.updateClusterLeaderSensor();

expect(sensor.state).toBe('node2');
expect(sensor.attributes.quorumReached).toBeTruthy();
});
});
99 changes: 99 additions & 0 deletions src/status/status.service.ts
@@ -0,0 +1,99 @@
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { EntitiesService } from '../entities/entities.service';
import { ClusterService } from '../cluster/cluster.service';
import { ConfigService } from '../config/config.service';
import { Sensor } from '../entities/sensor';
import { EntityCustomization } from '../entities/entity-customization.interface';
import { SensorConfig } from '../integrations/home-assistant/sensor-config';

@Injectable()
export class StatusService implements OnApplicationBootstrap {
private clusterSizeSensor: Sensor;
private clusterLeaderSensor: Sensor;

constructor(
private readonly entitiesService: EntitiesService,
private readonly clusterService: ClusterService,
private readonly configService: ConfigService
) {}

/**
* Lifecycle hook, called once the application has started.
*/
onApplicationBootstrap(): void {
this.clusterSizeSensor = this.createClusterSizeSensor();
this.clusterLeaderSensor = this.createClusterLeaderSensor();

this.updateClusterSizeSensor();
this.updateClusterLeaderSensor();

this.clusterService.on('added', this.updateClusterSizeSensor.bind(this));
this.clusterService.on('removed', this.updateClusterSizeSensor.bind(this));
this.clusterService.on('leader', this.updateClusterLeaderSensor.bind(this));
}

/**
* Updates the cluster size sensor based on the currently connected nodes.
*/
updateClusterSizeSensor(): void {
const nodes = Object.keys(this.clusterService.nodes());

this.clusterSizeSensor.state = nodes.length;
this.clusterSizeSensor.attributes.nodes = nodes;
}

/**
* Updates the cluster leader sensor based on the currently elected leader.
*/
updateClusterLeaderSensor(): void {
this.clusterLeaderSensor.state = this.clusterService.leader().id;
this.clusterLeaderSensor.attributes.quorumReached = this.clusterService.quorumReached();
}

/**
* Creates and registers a new cluster size sensor.
*
* @returns Registered sensor
*/
protected createClusterSizeSensor(): Sensor {
const instanceName = this.configService.get('global').instanceName;
const customizations: Array<EntityCustomization<any>> = [
{
for: SensorConfig,
overrides: {
icon: 'mdi:server',
unitOfMeasurement: 'instances'
}
}
];
const clusterSizeSensor = this.entitiesService.add(
new Sensor('status-cluster-size', `${instanceName} Cluster Size`),
customizations
);

return clusterSizeSensor as Sensor;
}

/**
* Creates and registers a new cluster leader sensor.
*
* @returns Registered sensor
*/
protected createClusterLeaderSensor(): Sensor {
const instanceName = this.configService.get('global').instanceName;
const customizations: Array<EntityCustomization<any>> = [
{
for: SensorConfig,
overrides: {
icon: 'mdi:account-group'
}
}
];
const clusterLeaderSensor = this.entitiesService.add(
new Sensor('status-cluster-leader', `${instanceName} Cluster Leader`),
customizations
);

return clusterLeaderSensor as Sensor;
}
}

0 comments on commit b8249e8

Please sign in to comment.