• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.adservices.data.adselection;
18 
19 import android.adservices.common.AdTechIdentifier;
20 import android.adservices.common.FrequencyCapFilters;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 import androidx.room.Dao;
25 import androidx.room.Insert;
26 import androidx.room.OnConflictStrategy;
27 import androidx.room.Query;
28 import androidx.room.Transaction;
29 
30 import com.android.adservices.service.adselection.HistogramEvent;
31 
32 import com.google.common.base.Preconditions;
33 
34 import java.time.Instant;
35 import java.util.Objects;
36 
37 /**
38  * DAO used to access ad counter histogram data used in frequency cap filtering during ad selection.
39  *
40  * <p>Annotated abstract methods will generate their own Room DB implementations.
41  */
42 @Dao
43 public abstract class FrequencyCapDao {
44     /**
45      * Attempts to persist a new {@link DBHistogramIdentifier} to the identifier table.
46      *
47      * <p>If there is already an identifier persisted with the same foreign key (see {@link
48      * DBHistogramIdentifier#getHistogramIdentifierForeignKey()}), the transaction is canceled and
49      * rolled back.
50      *
51      * <p>This method is not intended to be called on its own. Please use {@link
52      * #insertHistogramEvent(HistogramEvent, int, int)} instead.
53      *
54      * @return the row ID of the identifier in the table, or {@code -1} if the specified row ID is
55      *     already occupied
56      */
57     @Insert(onConflict = OnConflictStrategy.ABORT)
insertNewHistogramIdentifier(@onNull DBHistogramIdentifier identifier)58     protected abstract long insertNewHistogramIdentifier(@NonNull DBHistogramIdentifier identifier);
59 
60     /**
61      * Returns the foreign key ID for the identifier matching the given constraints, or {@code null}
62      * if no match is found.
63      *
64      * <p>If multiple matches are found, only the first (as ordered by the numerical ID) is
65      * returned.
66      *
67      * <p>This method is not intended to be called on its own. It should only be used in {@link
68      * #insertHistogramEvent(HistogramEvent, int, int)}.
69      *
70      * @return the row ID of the identifier in the table, or {@code null} if not found
71      */
72     @Query(
73             "SELECT foreign_key_id FROM fcap_histogram_ids "
74                     + "WHERE ad_counter_key = :adCounterKey "
75                     + "AND buyer = :buyer "
76                     // Note that the IS operator in SQLite specifically is equivalent to = for value
77                     // matching except that it also matches NULL
78                     + "AND custom_audience_owner IS :customAudienceOwner "
79                     + "AND custom_audience_name IS :customAudienceName "
80                     + "ORDER BY foreign_key_id ASC "
81                     + "LIMIT 1")
82     @Nullable
getHistogramIdentifierForeignKeyIfExists( @onNull String adCounterKey, @NonNull AdTechIdentifier buyer, @Nullable String customAudienceOwner, @Nullable String customAudienceName)83     protected abstract Long getHistogramIdentifierForeignKeyIfExists(
84             @NonNull String adCounterKey,
85             @NonNull AdTechIdentifier buyer,
86             @Nullable String customAudienceOwner,
87             @Nullable String customAudienceName);
88 
89     /**
90      * Attempts to persist a new {@link DBHistogramEventData} to the event data table.
91      *
92      * <p>If there is already an entry in the table with the same non-{@code null} row ID, the
93      * transaction is canceled and rolled back.
94      *
95      * <p>This method is not intended to be called on its own. Please use {@link
96      * #insertHistogramEvent(HistogramEvent, int, int)} instead.
97      *
98      * @return the row ID of the event data in the table, or -1 if the event data already exists
99      */
100     @Insert(onConflict = OnConflictStrategy.ABORT)
insertNewHistogramEventData(@onNull DBHistogramEventData eventData)101     protected abstract long insertNewHistogramEventData(@NonNull DBHistogramEventData eventData);
102 
103     /**
104      * Attempts to insert a {@link HistogramEvent} into the histogram tables in a single
105      * transaction.
106      *
107      * <p>If the current number of events in the histogram table is larger than the given {@code
108      * absoluteMaxHistogramEventCount}, then the oldest events in the table will be evicted so that
109      * the count of events is the given {@code lowerMaxHistogramEventCount}.
110      *
111      * @throws IllegalStateException if an error was encountered adding the event
112      */
113     @Transaction
insertHistogramEvent( @onNull HistogramEvent event, int absoluteMaxHistogramEventCount, int lowerMaxHistogramEventCount)114     public void insertHistogramEvent(
115             @NonNull HistogramEvent event,
116             int absoluteMaxHistogramEventCount,
117             int lowerMaxHistogramEventCount)
118             throws IllegalStateException {
119         Objects.requireNonNull(event);
120         Preconditions.checkArgument(absoluteMaxHistogramEventCount > 0);
121         Preconditions.checkArgument(lowerMaxHistogramEventCount > 0);
122         Preconditions.checkArgument(absoluteMaxHistogramEventCount > lowerMaxHistogramEventCount);
123 
124         // TODO(b/275581841): Collect and send telemetry on frequency cap eviction
125         // Check the table size first and evict older events if necessary
126         int currentHistogramEventCount = getTotalNumHistogramEvents();
127         if (currentHistogramEventCount >= absoluteMaxHistogramEventCount) {
128             int numEventsToDelete = currentHistogramEventCount - lowerMaxHistogramEventCount;
129             deleteOldestHistogramEventData(numEventsToDelete);
130             deleteUnpairedHistogramIdentifiers();
131         }
132 
133         // Converting to DBHistogramIdentifier drops custom audience fields if the type is WIN
134         DBHistogramIdentifier identifier = DBHistogramIdentifier.fromHistogramEvent(event);
135         Long foreignKeyId =
136                 getHistogramIdentifierForeignKeyIfExists(
137                         identifier.getAdCounterKey(),
138                         identifier.getBuyer(),
139                         identifier.getCustomAudienceOwner(),
140                         identifier.getCustomAudienceName());
141 
142         if (foreignKeyId == null) {
143             try {
144                 foreignKeyId = insertNewHistogramIdentifier(identifier);
145             } catch (Exception exception) {
146                 throw new IllegalStateException("Error inserting histogram identifier", exception);
147             }
148         }
149 
150         try {
151             insertNewHistogramEventData(
152                     DBHistogramEventData.fromHistogramEvent(foreignKeyId, event));
153         } catch (Exception exception) {
154             throw new IllegalStateException("Error inserting histogram event data", exception);
155         }
156     }
157 
158     /**
159      * Returns the number of events in the appropriate buyer's histograms that have been registered
160      * since the given timestamp.
161      *
162      * @return the number of found events that match the criteria
163      */
164     @Query(
165             "SELECT COUNT(DISTINCT data.row_id) FROM fcap_histogram_data AS data "
166                     + "INNER JOIN fcap_histogram_ids AS ids "
167                     + "ON data.foreign_key_id = ids.foreign_key_id "
168                     + "WHERE ids.ad_counter_key = :adCounterKey "
169                     + "AND ids.buyer = :buyer "
170                     + "AND data.ad_event_type = :adEventType "
171                     + "AND data.timestamp >= :startTime")
getNumEventsForBuyerAfterTime( @onNull String adCounterKey, @NonNull AdTechIdentifier buyer, @FrequencyCapFilters.AdEventType int adEventType, @NonNull Instant startTime)172     public abstract int getNumEventsForBuyerAfterTime(
173             @NonNull String adCounterKey,
174             @NonNull AdTechIdentifier buyer,
175             @FrequencyCapFilters.AdEventType int adEventType,
176             @NonNull Instant startTime);
177 
178     /**
179      * Returns the number of events in the appropriate custom audience's histogram that have been
180      * registered since the given timestamp.
181      *
182      * @return the number of found events that match the criteria
183      */
184     @Query(
185             "SELECT COUNT(DISTINCT data.row_id) FROM fcap_histogram_data AS data "
186                     + "INNER JOIN fcap_histogram_ids AS ids "
187                     + "ON data.foreign_key_id = ids.foreign_key_id "
188                     + "WHERE ids.ad_counter_key = :adCounterKey "
189                     + "AND ids.buyer = :buyer "
190                     + "AND ids.custom_audience_owner = :customAudienceOwner "
191                     + "AND ids.custom_audience_name = :customAudienceName "
192                     + "AND data.ad_event_type = :adEventType "
193                     + "AND data.timestamp >= :startTime")
getNumEventsForCustomAudienceAfterTime( @onNull String adCounterKey, @NonNull AdTechIdentifier buyer, @NonNull String customAudienceOwner, @NonNull String customAudienceName, @FrequencyCapFilters.AdEventType int adEventType, @NonNull Instant startTime)194     public abstract int getNumEventsForCustomAudienceAfterTime(
195             @NonNull String adCounterKey,
196             @NonNull AdTechIdentifier buyer,
197             @NonNull String customAudienceOwner,
198             @NonNull String customAudienceName,
199             @FrequencyCapFilters.AdEventType int adEventType,
200             @NonNull Instant startTime);
201 
202     /**
203      * Deletes all histogram event data older than the given {@code expiryTime}.
204      *
205      * <p>This method is not intended to be called on its own. Please use {@link
206      * #deleteAllExpiredHistogramData(Instant)} instead.
207      *
208      * @return the number of deleted events
209      */
210     @Query("DELETE FROM fcap_histogram_data WHERE timestamp < :expiryTime")
deleteHistogramEventDataBeforeTime(@onNull Instant expiryTime)211     protected abstract int deleteHistogramEventDataBeforeTime(@NonNull Instant expiryTime);
212 
213     /**
214      * Deletes the oldest N histogram events, where N is at most {@code numEventsToDelete}, and
215      * returns the number of entries deleted.
216      *
217      * <p>This method is not meant to be called on its own. Please use {@link
218      * #insertHistogramEvent(HistogramEvent, int, int)} to evict data when the table is full.
219      */
220     @Query(
221             "DELETE FROM fcap_histogram_data "
222                     + "WHERE row_id IN "
223                     + "(SELECT row_id FROM fcap_histogram_data "
224                     + "ORDER BY timestamp ASC "
225                     + "LIMIT :numEventsToDelete)")
deleteOldestHistogramEventData(int numEventsToDelete)226     protected abstract int deleteOldestHistogramEventData(int numEventsToDelete);
227 
228     /**
229      * Deletes histogram identifiers which have no associated event data.
230      *
231      * <p>This method is not intended to be called on its own. Please use {@link
232      * #deleteAllExpiredHistogramData(Instant)} instead.
233      *
234      * @return the number of deleted identifiers
235      */
236     @Query(
237             "DELETE FROM fcap_histogram_ids "
238                     + "WHERE foreign_key_id NOT IN "
239                     + "(SELECT ids.foreign_key_id FROM fcap_histogram_ids AS ids "
240                     + "INNER JOIN fcap_histogram_data AS data "
241                     + "ON ids.foreign_key_id = data.foreign_key_id)")
deleteUnpairedHistogramIdentifiers()242     protected abstract int deleteUnpairedHistogramIdentifiers();
243 
244     /**
245      * Deletes all histogram data older than the given {@code expiryTime} in a single database
246      * transaction.
247      *
248      * <p>Also cleans up any histogram identifiers which are no longer associated with any event
249      * data.
250      *
251      * @return the number of deleted events
252      */
253     @Transaction
deleteAllExpiredHistogramData(@onNull Instant expiryTime)254     public int deleteAllExpiredHistogramData(@NonNull Instant expiryTime) {
255         Objects.requireNonNull(expiryTime);
256 
257         int numDeletedEvents = deleteHistogramEventDataBeforeTime(expiryTime);
258         deleteUnpairedHistogramIdentifiers();
259         return numDeletedEvents;
260     }
261 
262     /** Returns the current total number of histogram events in the data table. */
263     @Query("SELECT COUNT(DISTINCT row_id) FROM fcap_histogram_data")
getTotalNumHistogramEvents()264     public abstract int getTotalNumHistogramEvents();
265 }
266