Track user activity in a persistent cache Add a new Groovy script `track-active-users-1.0.groovy` to track user activities by entering their `id` and timestamp into `track-active-users_cache` persistent cache. The tracking is implemented as a side effect on a new `GroupBackend` implementation that handles all group `UUID`'s. This way our backend `membershipsOf(CurrentUser)` method will be called whenever any group membership resolution is done. This gives us a single place to get the user activities for all supported entry points (UI, REST, SSH, git-over-http). The impact of on the user group resolution should be neglectable and should be equal to the performance of the persistent cache `get()` and `put()` calls. The user activity timestamp is stored with one-minute precision. Bear in mind, that in the case of git-over-http access to public repositories, even when the user provides authentication details, Gerrit will not consult any of the `GroupBackend`'s. This means that clones and fetches of publicly visible repositories will not be counted as user activity. Bug: Issue Issue 327730871 Change-Id: I5827288d40bba4c6b1e7b20d9aae822db8eabc29
diff --git a/admin/README.md b/admin/README.md index 7143588..9cca543 100644 --- a/admin/README.md +++ b/admin/README.md
@@ -11,3 +11,4 @@ * [warm-cache-1.0.groovy](/admin/warm-cache-1.0.groovy) - Controls the Gerrit cache warm-up via command-line * [readonly-1.0.groovy](/admin/readonly-1.0.groovy) - Set all Gerrit projects in read-only mode during maintenance * [stale-packed-refs-1.0.groovy](/admin/stale-packed-refs-1.0.groovy) - Check all specified projects and expose metric with age of `packed-refs.lock` files +* [track-active-users.groovy](/admin/track-active-users.groovy) - Tracks users login in `track-active-users_cache`
diff --git a/admin/track-active-users-1.0.groovy b/admin/track-active-users-1.0.groovy new file mode 100644 index 0000000..86a77a8 --- /dev/null +++ b/admin/track-active-users-1.0.groovy
@@ -0,0 +1,92 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import com.google.common.cache.* +import com.google.gerrit.common.* +import com.google.gerrit.entities.* +import com.google.gerrit.extensions.registration.* +import com.google.gerrit.server.* +import com.google.gerrit.server.account.* +import com.google.gerrit.server.cache.* +import com.google.gerrit.server.project.* +import com.google.inject.* +import com.google.inject.name.* + +import java.time.* + +import static java.util.concurrent.TimeUnit.* + +class TrackActiveUsersCache extends CacheModule { + static final NAME = "users_cache" + static final DEFAULT_CACHE_TTL = Duration.ofDays(90) + static final MAX_SIZE = 300_000 + + @Override + protected void configure() { + persist(NAME, Integer, Long) + .diskLimit(MAX_SIZE) + .maximumWeight(MAX_SIZE) + .expireAfterWrite(DEFAULT_CACHE_TTL) + } +} + +@Singleton +class TrackingGroupBackend implements GroupBackend { + @Inject + @Named(TrackActiveUsersCache.NAME) + Cache<Integer, Long> trackActiveUsersCache + + @Override + boolean handles(AccountGroup.UUID uuid) { + return true + } + + @Override + GroupDescription.Basic get(AccountGroup.UUID uuid) { + return null + } + + @Override + Collection<GroupReference> suggest(String name, @Nullable ProjectState project) { + return Collections.emptyList() + } + + @Override + GroupMembership membershipsOf(CurrentUser user) { + if (user.identifiedUser) { + def accountId = user.accountId.get() + def currentMinutes = MILLISECONDS.toMinutes(System.currentTimeMillis()) + if (trackActiveUsersCache.getIfPresent(accountId) != currentMinutes) { + trackActiveUsersCache.put(accountId, currentMinutes) + } + } + return GroupMembership.EMPTY + } + + @Override + boolean isVisibleToAll(AccountGroup.UUID uuid) { + return false + } +} + +class TrackActiveUsersModule extends AbstractModule { + @Override + void configure() { + install(new TrackActiveUsersCache()) + + DynamicSet.bind(binder(), GroupBackend).to(TrackingGroupBackend) + } +} + +modules = [TrackActiveUsersModule]