Skip to content

Commit 2b5b623

Browse files
authored
sa: truncate all timestamps to seconds (#7519)
As described in #7075, go-sql-driver/mysql v1.5.0 truncates timestamps to microseconds, while v1.6.0 and above does not. That means upon upgrading to v1.6.0, timestamps are written to the database with a resolution of nanoseconds, and SELECT statements also use a resolution of nanoseconds. We believe this is the cause of performance problems we observed when upgrading to v1.6.0 and above. To fix that, apply rounding in the application code. Rather than just rounding to microseconds, round to seconds since that is the resolution we care about. Using seconds rather than microseconds may also allow some of our indexes to grow more slowly over time. Note: this omits truncating some timestamps in CRL shard calculations, since truncating those resulted in test failures that I'll follow up on separately.
1 parent 5b64707 commit 2b5b623

File tree

5 files changed

+83
-51
lines changed

5 files changed

+83
-51
lines changed

cmd/bad-key-revoker/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func (bkr *badKeyRevoker) findUnrevoked(ctx context.Context, unchecked unchecked
141141
"SELECT id, certSerial FROM keyHashToSerial WHERE keyHash = ? AND id > ? AND certNotAfter > ? ORDER BY id LIMIT ?",
142142
unchecked.KeyHash,
143143
initialID,
144-
bkr.clk.Now(),
144+
bkr.clk.Now().Truncate(time.Second),
145145
bkr.serialBatchSize,
146146
)
147147
if err != nil {

docs/CONTRIBUTING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,13 @@ value. The `core.IsAnyNilOrZero` function can check these cases.
324324
Senders must check that timestamps are non-zero before sending them. Receivers
325325
must check that timestamps are non-zero before accepting them.
326326

327+
# Rounding time in DB
328+
329+
All times that we write to the database are truncated to one second's worth of
330+
precision. This reduces the size of indexes that include timestamps, and makes
331+
querying them more efficient. The Storage Authority (SA) is responsible for this
332+
truncation, and performs it for SELECT queries as well as INSERT and UPDATE.
333+
327334
# Release Process
328335

329336
The current Boulder release process is described in

sa/sa.go

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (ssa *SQLStorageAuthority) NewRegistration(ctx context.Context, req *corepb
108108
return nil, err
109109
}
110110

111-
reg.CreatedAt = ssa.clk.Now()
111+
reg.CreatedAt = ssa.clk.Now().Truncate(time.Second)
112112

113113
err = ssa.dbMap.Insert(ctx, reg)
114114
if err != nil {
@@ -141,6 +141,10 @@ func (ssa *SQLStorageAuthority) UpdateRegistration(ctx context.Context, req *cor
141141
return nil, err
142142
}
143143

144+
// The CreatedAt field shouldn't change from the original, so we copy it straight through.
145+
// This also ensures that it's already truncated to second (which happened on creation).
146+
update.CreatedAt = curr.CreatedAt
147+
144148
// Copy the existing registration model's LockCol to the new updated
145149
// registration model's LockCol
146150
update.LockCol = curr.LockCol
@@ -169,8 +173,8 @@ func (ssa *SQLStorageAuthority) AddSerial(ctx context.Context, req *sapb.AddSeri
169173
err := ssa.dbMap.Insert(ctx, &recordedSerialModel{
170174
Serial: req.Serial,
171175
RegistrationID: req.RegID,
172-
Created: req.Created.AsTime(),
173-
Expires: req.Expires.AsTime(),
176+
Created: req.Created.AsTime().Truncate(time.Second),
177+
Expires: req.Expires.AsTime().Truncate(time.Second),
174178
})
175179
if err != nil {
176180
return nil, err
@@ -223,7 +227,7 @@ func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb
223227
Serial: serialHex,
224228
RegistrationID: req.RegID,
225229
DER: req.Der,
226-
Issued: req.Issued.AsTime(),
230+
Issued: req.Issued.AsTime().Truncate(time.Second),
227231
Expires: parsed.NotAfter,
228232
}
229233

@@ -252,13 +256,15 @@ func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb
252256
cs := &core.CertificateStatus{
253257
Serial: serialHex,
254258
Status: status,
255-
OCSPLastUpdated: ssa.clk.Now(),
259+
OCSPLastUpdated: ssa.clk.Now().Truncate(time.Second),
256260
RevokedDate: time.Time{},
257261
RevokedReason: 0,
258262
LastExpirationNagSent: time.Time{},
259-
NotAfter: parsed.NotAfter,
260-
IsExpired: false,
261-
IssuerNameID: req.IssuerNameID,
263+
// No need to truncate because it's already truncated to encode
264+
// per https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5.1
265+
NotAfter: parsed.NotAfter,
266+
IsExpired: false,
267+
IssuerNameID: req.IssuerNameID,
262268
}
263269
err = ssa.dbMap.Insert(ctx, cs)
264270
if err != nil {
@@ -317,7 +323,7 @@ func (ssa *SQLStorageAuthority) AddCertificate(ctx context.Context, req *sapb.Ad
317323
Serial: serial,
318324
Digest: digest,
319325
DER: req.Der,
320-
Issued: req.Issued.AsTime(),
326+
Issued: req.Issued.AsTime().Truncate(time.Second),
321327
Expires: parsedCertificate.NotAfter,
322328
}
323329

@@ -476,13 +482,15 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
476482
if err != nil {
477483
return nil, err
478484
}
485+
// These parameters correspond to the fields listed in `authzFields`, as used in the
486+
// `db.NewMultiInserter` call above, and occur in the same order.
479487
err = inserter.Add([]interface{}{
480488
am.ID,
481489
am.IdentifierType,
482490
am.IdentifierValue,
483491
am.RegistrationID,
484492
statusToUint[core.StatusPending],
485-
am.Expires,
493+
am.Expires.Truncate(time.Second),
486494
am.Challenges,
487495
nil,
488496
nil,
@@ -503,11 +511,12 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
503511
// Second, insert the new order.
504512
var orderID int64
505513
var err error
506-
created := ssa.clk.Now()
514+
created := ssa.clk.Now().Truncate(time.Second)
515+
expires := req.NewOrder.Expires.AsTime().Truncate(time.Second)
507516
if features.Get().MultipleCertificateProfiles {
508517
omv2 := orderModelv2{
509518
RegistrationID: req.NewOrder.RegistrationID,
510-
Expires: req.NewOrder.Expires.AsTime(),
519+
Expires: expires,
511520
Created: created,
512521
CertificateProfileName: req.NewOrder.CertificateProfileName,
513522
}
@@ -516,7 +525,7 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
516525
} else {
517526
omv1 := orderModelv1{
518527
RegistrationID: req.NewOrder.RegistrationID,
519-
Expires: req.NewOrder.Expires.AsTime(),
528+
Expires: expires,
520529
Created: created,
521530
}
522531
err = tx.Insert(ctx, &omv1)
@@ -549,19 +558,25 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
549558
}
550559

551560
// Fourth, insert the FQDNSet entry for the order.
552-
err = addOrderFQDNSet(ctx, tx, req.NewOrder.Names, orderID, req.NewOrder.RegistrationID, req.NewOrder.Expires.AsTime())
561+
err = addOrderFQDNSet(ctx,
562+
tx,
563+
req.NewOrder.Names,
564+
orderID,
565+
req.NewOrder.RegistrationID,
566+
expires,
567+
)
553568
if err != nil {
554569
return nil, err
555570
}
556571

557-
// Finally, build the overall Order PB.
572+
// Finally, build the overall Order PB to return.
558573
res := &corepb.Order{
559574
// ID and Created were auto-populated on the order model when it was inserted.
560575
Id: orderID,
561576
Created: timestamppb.New(created),
562577
// These are carried over from the original request unchanged.
563578
RegistrationID: req.NewOrder.RegistrationID,
564-
Expires: req.NewOrder.Expires,
579+
Expires: timestamppb.New(expires),
565580
Names: req.NewOrder.Names,
566581
// Have to combine the already-associated and newly-reacted authzs.
567582
V2Authorizations: append(req.NewOrder.V2Authorizations, newAuthzIDs...),
@@ -576,7 +591,12 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
576591
if req.NewOrder.ReplacesSerial != "" {
577592
// Update the replacementOrders table to indicate that this order
578593
// replaces the provided certificate serial.
579-
err := addReplacementOrder(ctx, tx, req.NewOrder.ReplacesSerial, orderID, req.NewOrder.Expires.AsTime())
594+
err := addReplacementOrder(ctx,
595+
tx,
596+
req.NewOrder.ReplacesSerial,
597+
orderID,
598+
req.NewOrder.Expires.AsTime().Truncate(time.Second),
599+
)
580600
if err != nil {
581601
return nil, err
582602
}
@@ -789,7 +809,7 @@ func (ssa *SQLStorageAuthority) FinalizeAuthorization2(ctx context.Context, req
789809
// database attemptedAt field Null instead of 1970-01-01 00:00:00.
790810
var attemptedTime *time.Time
791811
if !core.IsAnyNilOrZero(req.AttemptedAt) {
792-
val := req.AttemptedAt.AsTime()
812+
val := req.AttemptedAt.AsTime().Truncate(time.Second)
793813
attemptedTime = &val
794814
}
795815
params := map[string]interface{}{
@@ -799,7 +819,7 @@ func (ssa *SQLStorageAuthority) FinalizeAuthorization2(ctx context.Context, req
799819
"validationRecord": vrJSON,
800820
"id": req.Id,
801821
"pending": statusUint(core.StatusPending),
802-
"expires": req.Expires.AsTime(),
822+
"expires": req.Expires.AsTime().Truncate(time.Second),
803823
// if req.ValidationError is nil veJSON should also be nil
804824
// which should result in a NULL field
805825
"validationError": veJSON,
@@ -867,7 +887,7 @@ func (ssa *SQLStorageAuthority) RevokeCertificate(ctx context.Context, req *sapb
867887
}
868888

869889
_, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
870-
revokedDate := req.Date.AsTime()
890+
revokedDate := req.Date.AsTime().Truncate(time.Second)
871891

872892
res, err := tx.ExecContext(ctx,
873893
`UPDATE certificateStatus SET
@@ -924,8 +944,8 @@ func (ssa *SQLStorageAuthority) UpdateRevokedCertificate(ctx context.Context, re
924944
}
925945

926946
_, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
927-
thisUpdate := req.Date.AsTime()
928-
revokedDate := req.Backdate.AsTime()
947+
thisUpdate := req.Date.AsTime().Truncate(time.Second)
948+
revokedDate := req.Backdate.AsTime().Truncate(time.Second)
929949

930950
res, err := tx.ExecContext(ctx,
931951
`UPDATE certificateStatus SET
@@ -1007,7 +1027,7 @@ func (ssa *SQLStorageAuthority) AddBlockedKey(ctx context.Context, req *sapb.Add
10071027
cols, qs := blockedKeysColumns, "?, ?, ?, ?"
10081028
vals := []interface{}{
10091029
req.KeyHash,
1010-
req.Added.AsTime(),
1030+
req.Added.AsTime().Truncate(time.Second),
10111031
sourceInt,
10121032
req.Comment,
10131033
}
@@ -1232,7 +1252,10 @@ func (ssa *SQLStorageAuthority) leaseSpecificCRLShard(ctx context.Context, req *
12321252

12331253
// UpdateCRLShard updates the thisUpdate and nextUpdate timestamps of a CRL
12341254
// shard. It rejects the update if it would cause the thisUpdate timestamp to
1235-
// move backwards. It does *not* reject the update if the shard is no longer
1255+
// move backwards, but if thisUpdate would stay the same (for instance, multiple
1256+
// CRL generations within a single second), it will succeed.
1257+
//
1258+
// It does *not* reject the update if the shard is no longer
12361259
// leased: although this would be unexpected (because the lease timestamp should
12371260
// be the same as the crl-updater's context expiration), it's not inherently a
12381261
// sign of an update that should be skipped. It does reject the update if the
@@ -1247,24 +1270,25 @@ func (ssa *SQLStorageAuthority) UpdateCRLShard(ctx context.Context, req *sapb.Up
12471270
// Only set the nextUpdate if it's actually present in the request message.
12481271
var nextUpdate *time.Time
12491272
if req.NextUpdate != nil {
1250-
nut := req.NextUpdate.AsTime()
1273+
nut := req.NextUpdate.AsTime().Truncate(time.Second)
12511274
nextUpdate = &nut
12521275
}
12531276

12541277
_, err := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
1278+
thisUpdate := req.ThisUpdate.AsTime().Truncate(time.Second)
12551279
res, err := tx.ExecContext(ctx,
12561280
`UPDATE crlShards
12571281
SET thisUpdate = ?, nextUpdate = ?, leasedUntil = ?
12581282
WHERE issuerID = ?
12591283
AND idx = ?
1260-
AND (thisUpdate is NULL OR thisUpdate < ?)
1284+
AND (thisUpdate is NULL OR thisUpdate <= ?)
12611285
LIMIT 1`,
1262-
req.ThisUpdate.AsTime(),
1286+
thisUpdate,
12631287
nextUpdate,
1264-
req.ThisUpdate.AsTime(),
1288+
thisUpdate,
12651289
req.IssuerNameID,
12661290
req.ShardIdx,
1267-
req.ThisUpdate.AsTime(),
1291+
thisUpdate,
12681292
)
12691293
if err != nil {
12701294
return nil, err
@@ -1275,7 +1299,7 @@ func (ssa *SQLStorageAuthority) UpdateCRLShard(ctx context.Context, req *sapb.Up
12751299
return nil, err
12761300
}
12771301
if rowsAffected == 0 {
1278-
return nil, fmt.Errorf("unable to update shard %d for issuer %d", req.ShardIdx, req.IssuerNameID)
1302+
return nil, fmt.Errorf("unable to update shard %d for issuer %d; possibly because shard exists", req.ShardIdx, req.IssuerNameID)
12791303
}
12801304
if rowsAffected != 1 {
12811305
return nil, errors.New("update affected unexpected number of rows")

sa/sa_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4012,7 +4012,7 @@ func TestUpdateCRLShard(t *testing.T) {
40124012
`SELECT thisUpdate FROM crlShards WHERE issuerID = 1 AND idx = 0 LIMIT 1`,
40134013
)
40144014
test.AssertNotError(t, err, "getting updated thisUpdate timestamp")
4015-
test.Assert(t, crlModel.ThisUpdate.Equal(thisUpdate), "checking updated thisUpdate timestamp")
4015+
test.AssertEquals(t, *crlModel.ThisUpdate, thisUpdate)
40164016

40174017
// Updating an unleased shard should work.
40184018
_, err = sa.UpdateCRLShard(

0 commit comments

Comments
 (0)