Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Makes it easier to debug and be notified on issues with the room-assistant cluster. Closes #127
- Loading branch information
Showing
4 changed files
with
241 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |