Skip to content

Commit cdbc50e

Browse files
committed
Implement job
1 parent 2749c91 commit cdbc50e

File tree

9 files changed

+164
-0
lines changed

9 files changed

+164
-0
lines changed

cli/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import (
7070
"github.com/coder/coder/coderd/database/migrations"
7171
"github.com/coder/coder/coderd/database/pubsub"
7272
"github.com/coder/coder/coderd/devtunnel"
73+
"github.com/coder/coder/coderd/dormancy"
7374
"github.com/coder/coder/coderd/gitauth"
7475
"github.com/coder/coder/coderd/gitsshkey"
7576
"github.com/coder/coder/coderd/httpapi"
@@ -812,6 +813,9 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
812813
options.SwaggerEndpoint = cfg.Swagger.Enable.Value()
813814
}
814815

816+
closeCheckInactiveUsersFunc := dormancy.CheckInactiveUsers(ctx, logger, options.Database)
817+
defer closeCheckInactiveUsersFunc()
818+
815819
// We use a separate coderAPICloser so the Enterprise API
816820
// can have it's own close functions. This is cleaner
817821
// than abstracting the Coder API itself.

coderd/database/dbauthz/dbauthz.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2104,6 +2104,13 @@ func (q *querier) UpdateGroupByID(ctx context.Context, arg database.UpdateGroupB
21042104
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateGroupByID)(ctx, arg)
21052105
}
21062106

2107+
func (q *querier) UpdateInactiveUsersToDormant(ctx context.Context, lastSeenAfter time.Time) ([]database.UpdateInactiveUsersToDormantRow, error) {
2108+
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
2109+
return nil, err
2110+
}
2111+
return q.db.UpdateInactiveUsersToDormant(ctx, lastSeenAfter)
2112+
}
2113+
21072114
func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
21082115
// Authorized fetch will check that the actor has read access to the org member since the org member is returned.
21092116
member, err := q.GetOrganizationMemberByUserID(ctx, database.GetOrganizationMemberByUserIDParams{

coderd/database/dbfake/dbfake.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4337,6 +4337,28 @@ func (q *FakeQuerier) UpdateGroupByID(_ context.Context, arg database.UpdateGrou
43374337
return database.Group{}, sql.ErrNoRows
43384338
}
43394339

4340+
func (q *FakeQuerier) UpdateInactiveUsersToDormant(ctx context.Context, lastSeenAfter time.Time) ([]database.UpdateInactiveUsersToDormantRow, error) {
4341+
q.mutex.Lock()
4342+
defer q.mutex.Unlock()
4343+
4344+
var updated []database.UpdateInactiveUsersToDormantRow
4345+
for index, user := range q.users {
4346+
if user.Status == database.UserStatusActive && user.LastSeenAt.Before(lastSeenAfter) {
4347+
q.users[index].Status = database.UserStatusDormant
4348+
updated = append(updated, database.UpdateInactiveUsersToDormantRow{
4349+
ID: user.ID,
4350+
Email: user.Email,
4351+
LastSeenAt: user.LastSeenAt,
4352+
})
4353+
}
4354+
}
4355+
4356+
if len(updated) == 0 {
4357+
return nil, sql.ErrNoRows
4358+
}
4359+
return updated, nil
4360+
}
4361+
43404362
func (q *FakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
43414363
if err := validateDatabaseType(arg); err != nil {
43424364
return database.OrganizationMember{}, err

coderd/database/dbmetrics/dbmetrics.go

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

coderd/database/dbmock/dbmock.go

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

coderd/database/querier.go

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

coderd/database/queries.sql.go

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

coderd/database/queries/users.sql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,14 @@ SET
250250
WHERE
251251
id = $1
252252
RETURNING *;
253+
254+
255+
-- name: UpdateInactiveUsersToDormant :many
256+
UPDATE
257+
users
258+
SET
259+
user_status = 'dormant'::user_status
260+
WHERE
261+
last_seen_at < @last_seen_after :: timestamp
262+
AND user_status = 'active'::user_status
263+
RETURNING id, email, last_seen_at;

coderd/dormancy/dormantusersjob.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package dormancy
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"time"
7+
8+
"golang.org/x/xerrors"
9+
10+
"cdr.dev/slog"
11+
12+
"github.com/coder/coder/coderd/database"
13+
)
14+
15+
const (
16+
checkDuration = 5 * time.Minute
17+
dormancyPeriod = 90 * 24 * time.Hour
18+
)
19+
20+
func CheckInactiveUsers(ctx context.Context, logger slog.Logger, db database.Store) func() {
21+
logger = logger.Named("dormancy")
22+
23+
ctx, cancelFunc := context.WithCancel(ctx)
24+
done := make(chan struct{})
25+
ticker := time.NewTicker(checkDuration)
26+
go func() {
27+
defer close(done)
28+
defer ticker.Stop()
29+
for {
30+
select {
31+
case <-ctx.Done():
32+
return
33+
case <-ticker.C:
34+
}
35+
36+
lastSeenAfter := database.Now().Add(-dormancyPeriod)
37+
logger.Debug(ctx, "check inactive user accounts", slog.F("dormancy_period", dormancyPeriod), slog.F("last_seen_after", lastSeenAfter))
38+
39+
updatedUsers, err := db.UpdateInactiveUsersToDormant(ctx, lastSeenAfter)
40+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
41+
logger.Error(ctx, "can't mark inactive users as dormant", slog.Error(err))
42+
goto done
43+
}
44+
45+
for _, u := range updatedUsers {
46+
logger.Debug(ctx, "account has been marked as dormant", slog.F("email", u.Email), slog.F("last_seen_at", u.LastSeenAt))
47+
}
48+
done:
49+
logger.Debug(ctx, "checking user accounts is done", slog.F("num_dormant_accounts", len(updatedUsers)))
50+
}
51+
}()
52+
53+
return func() {
54+
cancelFunc()
55+
<-done
56+
}
57+
}

0 commit comments

Comments
 (0)