Skip to content

Commit d95ecf4

Browse files
authored
Merge pull request #75 from warioishere/codex/update-savecurrent-to-increment-count
Fix pool and client rejected stats flush increments
2 parents ae08dd6 + 652ea43 commit d95ecf4

12 files changed

Lines changed: 528 additions & 76 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Welcome to **BlitzPool**, a lightweight and open-source Bitcoin mining pool based on the [public-pool](https://github.com/benjamin-wilson/public-pool) project – extended with powerful new features and real-world integrations.
44

5-
Current Version: **v1.3.2**
5+
Current Version: **v1.3.3**
66

77
🌐 **Live Pool:** [https://blitzpool.yourdevice.ch/#/](https://blitzpool.yourdevice.ch/#/)
88

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "public-pool",
3-
"version": "1.3.2",
3+
"version": "1.3.3",
44
"description": "",
55
"author": "",
66
"private": true,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { DataSource } from 'typeorm';
2+
3+
import { ClientRejectedStatisticsEntity } from './client-rejected-statistics.entity';
4+
import { ClientRejectedStatisticsService } from './client-rejected-statistics.service';
5+
6+
describe('ClientRejectedStatisticsService', () => {
7+
let dataSource: DataSource;
8+
9+
beforeEach(async () => {
10+
dataSource = new DataSource({
11+
type: 'sqlite',
12+
database: ':memory:',
13+
entities: [ClientRejectedStatisticsEntity],
14+
synchronize: true,
15+
});
16+
await dataSource.initialize();
17+
});
18+
19+
afterEach(async () => {
20+
if (dataSource?.isInitialized) {
21+
await dataSource.destroy();
22+
}
23+
});
24+
25+
it('accumulates deltas when flushing over an existing row', async () => {
26+
const repository = dataSource.getRepository(ClientRejectedStatisticsEntity);
27+
const service = new ClientRejectedStatisticsService(repository as any);
28+
29+
const tenMinutes = 1000 * 60 * 10;
30+
const now = 1700000000000;
31+
const timeSlot = Math.floor(now / tenMinutes) * tenMinutes;
32+
33+
await repository.insert({
34+
address: 'addr',
35+
reason: 'duplicate',
36+
time: timeSlot,
37+
count: 10,
38+
shares: 100,
39+
});
40+
41+
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(timeSlot + 5_000);
42+
43+
try {
44+
await service.addRejectedShare('addr', 'duplicate', 5);
45+
await (service as any).saveCurrent();
46+
47+
let row = await repository.findOneBy({
48+
address: 'addr',
49+
reason: 'duplicate',
50+
time: timeSlot,
51+
});
52+
53+
expect(row).toMatchObject({ count: 11, shares: 104 });
54+
55+
await service.addRejectedShare('addr', 'duplicate', 3);
56+
await (service as any).saveCurrent();
57+
58+
row = await repository.findOneBy({
59+
address: 'addr',
60+
reason: 'duplicate',
61+
time: timeSlot,
62+
});
63+
64+
expect(row).toMatchObject({ count: 12, shares: 106 });
65+
} finally {
66+
nowSpy.mockRestore();
67+
}
68+
});
69+
});

src/ORM/client-rejected-statistics/client-rejected-statistics.service.ts

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,32 +36,14 @@ export class ClientRejectedStatisticsService {
3636

3737
if (this.currentTimeSlot == null) {
3838
this.currentTimeSlot = timeSlot;
39-
const existing = await this.clientRejectedStatisticsRepository.findBy({ time: timeSlot });
4039
this.counts.clear();
41-
for (const rec of existing) {
42-
if (!this.counts.has(rec.address)) {
43-
this.counts.set(rec.address, new Map());
44-
}
45-
this.counts
46-
.get(rec.address)
47-
.set(rec.reason, { count: rec.count, shares: rec.shares ?? 0 });
48-
}
4940
this.lastSave = now;
5041
}
5142

5243
if (this.currentTimeSlot !== timeSlot) {
5344
await this.saveCurrent();
5445
this.currentTimeSlot = timeSlot;
55-
const existing = await this.clientRejectedStatisticsRepository.findBy({ time: timeSlot });
5646
this.counts.clear();
57-
for (const rec of existing) {
58-
if (!this.counts.has(rec.address)) {
59-
this.counts.set(rec.address, new Map());
60-
}
61-
this.counts
62-
.get(rec.address)
63-
.set(rec.reason, { count: rec.count, shares: rec.shares ?? 0 });
64-
}
6547
this.lastSave = now;
6648
}
6749

@@ -83,29 +65,51 @@ export class ClientRejectedStatisticsService {
8365
}
8466

8567
private async saveCurrent() {
86-
for (const [address, reasons] of this.counts) {
87-
for (const [reason, stats] of reasons) {
88-
const existing = await this.clientRejectedStatisticsRepository.findOneBy({
68+
if (this.counts.size === 0) {
69+
return;
70+
}
71+
72+
const values: Array<{
73+
time: number;
74+
address: string;
75+
reason: string;
76+
count: number;
77+
shares: number;
78+
}> = [];
79+
80+
for (const [address, reasons] of this.counts.entries()) {
81+
for (const [reason, stats] of reasons.entries()) {
82+
if (stats.count === 0 && stats.shares === 0) {
83+
continue;
84+
}
85+
86+
values.push({
8987
time: this.currentTimeSlot,
9088
address,
9189
reason,
90+
count: stats.count,
91+
shares: stats.shares,
9292
});
93-
if (existing) {
94-
await this.clientRejectedStatisticsRepository.update(
95-
{ time: this.currentTimeSlot, address, reason },
96-
{ count: stats.count, shares: stats.shares, updatedAt: new Date() },
97-
);
98-
} else {
99-
await this.clientRejectedStatisticsRepository.insert({
100-
time: this.currentTimeSlot,
101-
address,
102-
reason,
103-
count: stats.count,
104-
shares: stats.shares,
105-
});
106-
}
10793
}
10894
}
95+
96+
if (values.length === 0) {
97+
this.counts.clear();
98+
return;
99+
}
100+
101+
await this.clientRejectedStatisticsRepository
102+
.createQueryBuilder()
103+
.insert()
104+
.into(ClientRejectedStatisticsEntity)
105+
.values(values)
106+
.onConflict(
107+
'("time", "address", "reason") DO UPDATE SET "count" = "count" + EXCLUDED."count", "shares" = COALESCE("shares", 0) + EXCLUDED."shares", "updatedAt" = :updatedAt',
108+
)
109+
.setParameters({ updatedAt: new Date() })
110+
.execute();
111+
112+
this.counts.clear();
109113
}
110114

111115
public async getTotalsSince(

src/ORM/pool-rejected-statistics/pool-rejected-statistics.service.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -86,22 +86,14 @@ export class PoolRejectedStatisticsService {
8686

8787
if (this.currentTimeSlot == null) {
8888
this.currentTimeSlot = timeSlot;
89-
const existing = await this.poolRejectedStatisticsRepository.findBy({ time: timeSlot });
9089
this.counts.clear();
91-
for (const rec of existing) {
92-
this.counts.set(rec.reason, rec.count);
93-
}
9490
this.lastSave = now;
9591
}
9692

9793
if (this.currentTimeSlot !== timeSlot) {
9894
await this.saveCurrent();
9995
this.currentTimeSlot = timeSlot;
100-
const existing = await this.poolRejectedStatisticsRepository.findBy({ time: timeSlot });
10196
this.counts.clear();
102-
for (const rec of existing) {
103-
this.counts.set(rec.reason, rec.count);
104-
}
10597
this.lastSave = now;
10698
}
10799

@@ -140,17 +132,35 @@ export class PoolRejectedStatisticsService {
140132
}
141133

142134
private async saveCurrent() {
143-
for (const [reason, count] of this.counts) {
144-
const existing = await this.poolRejectedStatisticsRepository.findOneBy({ time: this.currentTimeSlot, reason });
145-
if (existing) {
146-
await this.poolRejectedStatisticsRepository.update(
147-
{ time: this.currentTimeSlot, reason },
148-
{ count, updatedAt: new Date() },
149-
);
150-
} else {
151-
await this.poolRejectedStatisticsRepository.insert({ time: this.currentTimeSlot, reason, count });
152-
}
135+
if (this.counts.size === 0) {
136+
return;
153137
}
138+
139+
const values = Array.from(this.counts.entries())
140+
.filter(([, delta]) => delta !== 0)
141+
.map(([reason, delta]) => ({
142+
time: this.currentTimeSlot,
143+
reason,
144+
count: delta,
145+
}));
146+
147+
if (values.length === 0) {
148+
this.counts.clear();
149+
return;
150+
}
151+
152+
await this.poolRejectedStatisticsRepository
153+
.createQueryBuilder()
154+
.insert()
155+
.into(PoolRejectedStatisticsEntity)
156+
.values(values)
157+
.onConflict(
158+
'("time", "reason") DO UPDATE SET "count" = "count" + EXCLUDED."count", "updatedAt" = :updatedAt',
159+
)
160+
.setParameters({ updatedAt: new Date() })
161+
.execute();
162+
163+
this.counts.clear();
154164
}
155165

156166
public async getTotalsSince(time: number): Promise<Record<string, number>> {

0 commit comments

Comments
 (0)