Skip to content

Commit d133ab4

Browse files
committed
history: Use initial timestamp for history retention
The history retention is currently based on the final or closing timestamp, if such a timestamp exists. This change also takes the initial timestamp into account as a fallback. As reported in the Community Forum[^0] and in an ongoing PR[^1], there might be (historical) history entries without a closing timestamp. Thus, these entries will never be rotated. Furthermore, based on the documentation, it might be expected that the initial timestamp is used for retention, not a later one. This change does not cover the SLA retention, to ensure no misleading SLA reports are being generated when a dangerously short SLA retention period was defined. [^0]: https://community.icinga.com/t/comment-history-and-retention-on-a-busy-system/15318/3 [^1]: #913
1 parent 6400506 commit d133ab4

3 files changed

Lines changed: 106 additions & 63 deletions

File tree

pkg/icingadb/cleanup.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@ import (
88
"github.com/icinga/icinga-go-library/database"
99
"github.com/icinga/icinga-go-library/retry"
1010
"github.com/icinga/icinga-go-library/types"
11+
"strings"
1112
"time"
1213
)
1314

1415
// CleanupStmt defines information needed to compose cleanup statements.
16+
//
17+
// When multiple Columns are given, COALESCE is being applied, returning the
18+
// first non NULL column. Thus, start by supplying the timestamp column expected
19+
// to appear later, e.g., remove_time before entry_time.
1520
type CleanupStmt struct {
16-
Table string
17-
PK string
18-
Column string
21+
Table string
22+
PK string
23+
Columns []string
1924
}
2025

2126
// CleanupOlderThan deletes all rows with the specified statement that are older than the given time.
@@ -78,15 +83,20 @@ func (stmt *CleanupStmt) CleanupOlderThan(
7883

7984
// build assembles the cleanup statement for the specified database driver with the given limit.
8085
func (stmt *CleanupStmt) build(driverName string, limit uint64) string {
86+
if len(stmt.Columns) == 0 {
87+
panic("CleanupStmt.Columns must not be empty")
88+
}
89+
timeCol := "COALESCE(" + strings.Join(stmt.Columns, ",") + ")"
90+
8191
switch driverName {
8292
case database.MySQL:
8393
return fmt.Sprintf(`DELETE FROM %[1]s WHERE environment_id = :environment_id AND %[2]s < :time LIMIT %[3]d`,
84-
stmt.Table, stmt.Column, limit)
94+
stmt.Table, timeCol, limit)
8595
case database.PostgreSQL:
8696
return fmt.Sprintf(`WITH rows AS (
8797
SELECT %[1]s FROM %[2]s WHERE environment_id = :environment_id AND %[3]s < :time LIMIT %[4]d
8898
)
89-
DELETE FROM %[2]s WHERE %[1]s IN (SELECT %[1]s FROM rows)`, stmt.PK, stmt.Table, stmt.Column, limit)
99+
DELETE FROM %[2]s WHERE %[1]s IN (SELECT %[1]s FROM rows)`, stmt.PK, stmt.Table, timeCol, limit)
90100
default:
91101
panic(fmt.Sprintf("invalid database type %s", driverName))
92102
}

pkg/icingadb/history/retention.go

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,69 +30,76 @@ type retentionStatement struct {
3030
}
3131

3232
// RetentionStatements maps history categories with corresponding cleanup statements.
33+
//
34+
// For history retention, both the initial timestamp and the closing timestamp
35+
// are taken into account. The latter takes precedence, but might be NULL for
36+
// some forgotten objects, e.g., for comments.
37+
//
38+
// However, SLA retention only cares about the closing timestamp to ensure that a short retention
39+
// period will not mess up SLA calculations.
3340
var RetentionStatements = []retentionStatement{{
3441
RetentionType: RetentionHistory,
3542
Category: "acknowledgement",
3643
CleanupStmt: icingadb.CleanupStmt{
37-
Table: "acknowledgement_history",
38-
PK: "id",
39-
Column: "clear_time",
44+
Table: "acknowledgement_history",
45+
PK: "id",
46+
Columns: []string{"clear_time", "set_time"},
4047
},
4148
}, {
4249
RetentionType: RetentionHistory,
4350
Category: "comment",
4451
CleanupStmt: icingadb.CleanupStmt{
45-
Table: "comment_history",
46-
PK: "comment_id",
47-
Column: "remove_time",
52+
Table: "comment_history",
53+
PK: "comment_id",
54+
Columns: []string{"remove_time", "entry_time"},
4855
},
4956
}, {
5057
RetentionType: RetentionHistory,
5158
Category: "downtime",
5259
CleanupStmt: icingadb.CleanupStmt{
53-
Table: "downtime_history",
54-
PK: "downtime_id",
55-
Column: "end_time",
60+
Table: "downtime_history",
61+
PK: "downtime_id",
62+
Columns: []string{"end_time", "entry_time"},
5663
},
5764
}, {
5865
RetentionType: RetentionHistory,
5966
Category: "flapping",
6067
CleanupStmt: icingadb.CleanupStmt{
61-
Table: "flapping_history",
62-
PK: "id",
63-
Column: "end_time",
68+
Table: "flapping_history",
69+
PK: "id",
70+
Columns: []string{"end_time", "start_time"},
6471
},
6572
}, {
6673
RetentionType: RetentionHistory,
6774
Category: "notification",
6875
CleanupStmt: icingadb.CleanupStmt{
69-
Table: "notification_history",
70-
PK: "id",
71-
Column: "send_time",
76+
Table: "notification_history",
77+
PK: "id",
78+
Columns: []string{"send_time"},
7279
},
7380
}, {
7481
RetentionType: RetentionHistory,
7582
Category: "state",
7683
CleanupStmt: icingadb.CleanupStmt{
77-
Table: "state_history",
78-
PK: "id",
79-
Column: "event_time",
84+
Table: "state_history",
85+
PK: "id",
86+
Columns: []string{"event_time"},
8087
},
8188
}, {
8289
RetentionType: RetentionSla,
8390
Category: "sla_downtime",
8491
CleanupStmt: icingadb.CleanupStmt{
85-
Table: "sla_history_downtime",
86-
PK: "downtime_id",
87-
Column: "downtime_end",
92+
Table: "sla_history_downtime",
93+
PK: "downtime_id",
94+
Columns: []string{"downtime_end"},
8895
},
8996
}, {
9097
RetentionType: RetentionSla,
9198
Category: "sla_state",
9299
CleanupStmt: icingadb.CleanupStmt{
93-
Table: "sla_history_state",
94-
PK: "id",
95-
Column: "event_time",
100+
Table: "sla_history_state",
101+
PK: "id",
102+
Columns: []string{"event_time"},
96103
},
97104
}}
98105

tests/cleanup_and_retention_test.go

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/jmoiron/sqlx"
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
13+
"slices"
1314
"strings"
1415
"testing"
1516
"time"
@@ -88,8 +89,15 @@ func TestCleanupAndRetention(t *testing.T) {
8889
values = append(values, row{otherEnvId, getId(), startMilli - int64(j)})
8990
}
9091

91-
_, err = db.NamedExec(fmt.Sprintf(`INSERT INTO %s (environment_id, %s, %s) VALUES (:env, :id, :time)`,
92-
stmt.Table, stmt.PK, stmt.Column), values)
92+
timeColumns := strings.Join(
93+
append(stmt.ExtraTimeColumns, stmt.TimeColumn),
94+
", ")
95+
timeColumnsValuePlaceholder := strings.Join(
96+
slices.Repeat([]string{":time"}, 1+len(stmt.ExtraTimeColumns)),
97+
", ")
98+
99+
_, err = db.NamedExec(fmt.Sprintf(`INSERT INTO %s (environment_id, %s, %s) VALUES (:env, :id, %s)`,
100+
stmt.Table, stmt.PK, timeColumns, timeColumnsValuePlaceholder), values)
93101
require.NoError(t, err)
94102
}
95103

@@ -108,15 +116,15 @@ func TestCleanupAndRetention(t *testing.T) {
108116

109117
var rowsLeft int
110118
err := db.QueryRow(
111-
db.Rebind(fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE environment_id = ? AND %s < ?`, stmt.Table, stmt.Column)),
119+
db.Rebind(fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE environment_id = ? AND %s < ?`, stmt.Table, stmt.TimeColumn)),
112120
envId,
113121
thresholdMilli,
114122
).Scan(&rowsLeft)
115123
assert.NoError(t, err)
116124

117125
var rowsSpared int
118126
err = db.QueryRow(
119-
db.Rebind(fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE environment_id = ? AND %s >= ?`, stmt.Table, stmt.Column)),
127+
db.Rebind(fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE environment_id = ? AND %s >= ?`, stmt.Table, stmt.TimeColumn)),
120128
envId,
121129
thresholdMilli,
122130
).Scan(&rowsSpared)
@@ -143,9 +151,10 @@ func TestCleanupAndRetention(t *testing.T) {
143151
}
144152

145153
type cleanupStmt struct {
146-
Table string
147-
PK string
148-
Column string
154+
Table string
155+
PK string
156+
TimeColumn string
157+
ExtraTimeColumns []string
149158
}
150159

151160
type retention struct {
@@ -156,44 +165,48 @@ type retention struct {
156165

157166
var retentionStatements = map[string]cleanupStmt{
158167
"acknowledgement": {
159-
Table: "acknowledgement_history",
160-
PK: "id",
161-
Column: "clear_time",
168+
Table: "acknowledgement_history",
169+
PK: "id",
170+
TimeColumn: "clear_time",
171+
ExtraTimeColumns: []string{"set_time"},
162172
},
163173
"comment": {
164-
Table: "comment_history",
165-
PK: "comment_id",
166-
Column: "remove_time",
174+
Table: "comment_history",
175+
PK: "comment_id",
176+
TimeColumn: "remove_time",
177+
ExtraTimeColumns: []string{"entry_time"},
167178
},
168179
"downtime": {
169-
Table: "downtime_history",
170-
PK: "downtime_id",
171-
Column: "end_time",
180+
Table: "downtime_history",
181+
PK: "downtime_id",
182+
TimeColumn: "end_time",
183+
ExtraTimeColumns: []string{"entry_time"},
172184
},
173185
"flapping": {
174-
Table: "flapping_history",
175-
PK: "id",
176-
Column: "end_time",
186+
Table: "flapping_history",
187+
PK: "id",
188+
TimeColumn: "end_time",
189+
ExtraTimeColumns: []string{"start_time"},
177190
},
178191
"notification": {
179-
Table: "notification_history",
180-
PK: "id",
181-
Column: "send_time",
192+
Table: "notification_history",
193+
PK: "id",
194+
TimeColumn: "send_time",
182195
},
183196
"state": {
184-
Table: "state_history",
185-
PK: "id",
186-
Column: "event_time",
197+
Table: "state_history",
198+
PK: "id",
199+
TimeColumn: "event_time",
187200
},
188201
"sla_downtime": {
189-
Table: "sla_history_downtime",
190-
PK: "downtime_id",
191-
Column: "downtime_end",
202+
Table: "sla_history_downtime",
203+
PK: "downtime_id",
204+
TimeColumn: "downtime_end",
192205
},
193206
"sla_state": {
194-
Table: "sla_history_state",
195-
PK: "id",
196-
Column: "event_time",
207+
Table: "sla_history_state",
208+
PK: "id",
209+
TimeColumn: "event_time",
197210
},
198211
}
199212

@@ -208,13 +221,26 @@ func dropNotNullColumns(db *sqlx.DB, stmt cleanupStmt) error {
208221
schema = `CURRENT_SCHEMA()`
209222
}
210223

224+
columnNames := append(
225+
[]string{"environment_id", stmt.PK, stmt.TimeColumn},
226+
stmt.ExtraTimeColumns...)
227+
columnNamesPlaceholder := strings.Join(
228+
slices.Repeat([]string{"?"}, len(columnNames)),
229+
", ")
230+
231+
colsArgs := []any{stmt.Table}
232+
for _, columnName := range columnNames {
233+
colsArgs = append(colsArgs, columnName)
234+
}
235+
colsArgs = append(colsArgs, "NO")
236+
211237
var cols []string
212238
err := db.Select(&cols, db.Rebind(fmt.Sprintf(`
213239
SELECT column_name
214240
FROM information_schema.columns
215-
WHERE table_schema = %s AND table_name = ? AND column_name NOT IN (?, ?, ?) AND is_nullable = ?`,
216-
schema)),
217-
stmt.Table, "environment_id", stmt.PK, stmt.Column, "NO")
241+
WHERE table_schema = %s AND table_name = ? AND column_name NOT IN (%s) AND is_nullable = ?`,
242+
schema, columnNamesPlaceholder)),
243+
colsArgs...)
218244
if err != nil {
219245
return err
220246
}

0 commit comments

Comments
 (0)