11package feast .serving .service ;
22
3+ import static feast .serving .util .BigQueryUtil .getTimestampLimitQuery ;
4+
5+ import com .google .api .services .bigquery .model .TableReference ;
36import com .google .cloud .bigquery .BigQuery ;
7+ import com .google .cloud .bigquery .BigQuery .TableField ;
8+ import com .google .cloud .bigquery .BigQuery .TableOption ;
49import com .google .cloud .bigquery .BigQueryException ;
10+ import com .google .cloud .bigquery .Dataset ;
511import com .google .cloud .bigquery .DatasetId ;
612import com .google .cloud .bigquery .ExtractJobConfiguration ;
713import com .google .cloud .bigquery .Field ;
14+ import com .google .cloud .bigquery .FieldValueList ;
815import com .google .cloud .bigquery .FormatOptions ;
916import com .google .cloud .bigquery .Job ;
1017import com .google .cloud .bigquery .JobInfo ;
1522import com .google .cloud .bigquery .TableDefinition ;
1623import com .google .cloud .bigquery .TableId ;
1724import com .google .cloud .bigquery .TableInfo ;
25+ import com .google .cloud .bigquery .TableResult ;
1826import com .google .cloud .storage .Blob ;
1927import com .google .cloud .storage .Storage ;
2028import com .google .cloud .storage .Storage .BlobListOption ;
3644import feast .serving .ServingAPIProto .JobType ;
3745import feast .serving .util .BigQueryUtil ;
3846import io .grpc .Status ;
47+ import java .io .IOException ;
3948import java .util .ArrayList ;
4049import java .util .List ;
4150import java .util .Optional ;
4655@ Slf4j
4756public class BigQueryServingService implements ServingService {
4857
58+ private static final Long TABLE_EXPIRATION_TIME = 172800000L ;
59+
4960 private final BigQuery bigquery ;
5061 private final String projectId ;
5162 private final String datasetId ;
@@ -112,19 +123,34 @@ public GetBatchFeaturesResponse getBatchFeatures(GetBatchFeaturesRequest getFeat
112123 }
113124
114125 Table entityTable = loadEntities (getFeaturesRequest .getDatasetSource ());
126+ String entityTableName = entityTable .getTableId ().getTable ();
127+ //TODO: add expiration to temp tables
128+ // entityTable = entityTable.toBuilder().setExpirationTime(TABLE_EXPIRATION_TIME).build();
129+ // entityTable.update(TableOption.fields(TableField.EXPIRATION_TIME));
130+ FieldValueList timestampLimits = getTimestampLimits (entityTableName );
131+
115132 Schema entityTableSchema = entityTable .getDefinition ().getSchema ();
116133 List <String > entityNames = entityTableSchema .getFields ().stream ()
117134 .map (Field ::getName )
118135 .filter (name -> !name .equals ("event_timestamp" ))
119136 .collect (Collectors .toList ());
120137
121- final String query =
122- BigQueryUtil .createQuery (
123- getFeaturesRequest .getFeatureSetsList (),
124- featureSetSpecs ,
125- entityNames ,
126- datasetId , entityTable .getFriendlyName ());
127- log .debug ("Running BigQuery query: {}" , query );
138+ String query ;
139+ try {
140+ query =
141+ BigQueryUtil .createQuery (
142+ getFeaturesRequest .getFeatureSetsList (),
143+ featureSetSpecs ,
144+ entityNames ,
145+ projectId ,
146+ datasetId ,
147+ entityTableName ,
148+ timestampLimits .get ("min" ).getStringValue (),
149+ timestampLimits .get ("max" ).getStringValue ());
150+ log .info ("Running BigQuery query: {}" , query );
151+ } catch (IOException e ) {
152+ throw new RuntimeException ("Unable to generate query for batch retrieval" );
153+ }
128154
129155 String feastJobId = UUID .randomUUID ().toString ();
130156 ServingAPIProto .Job feastJob =
@@ -217,6 +243,32 @@ public GetBatchFeaturesResponse getBatchFeatures(GetBatchFeaturesRequest getFeat
217243 return GetBatchFeaturesResponse .newBuilder ().setJob (feastJob ).build ();
218244 }
219245
246+ private FieldValueList getTimestampLimits (String entityTableName ) {
247+ QueryJobConfiguration getTimestampLimitsQuery = QueryJobConfiguration
248+ .newBuilder (getTimestampLimitQuery (projectId , datasetId , entityTableName ))
249+ .setDefaultDataset (DatasetId .of (projectId , datasetId )).build ();
250+ try {
251+ Job job = bigquery
252+ .create (JobInfo .of (getTimestampLimitsQuery ));
253+ TableResult getTimestampLimitsQueryResult = job
254+ .waitFor ()
255+ .getQueryResults ();
256+ FieldValueList result = null ;
257+ for (FieldValueList fields : getTimestampLimitsQueryResult .getValues ()) {
258+ result = fields ;
259+ }
260+ if (result == null || result .get ("min" ).isNull () || result .get ("max" ).isNull ()) {
261+ throw new RuntimeException ("query returned insufficient values" );
262+ }
263+ return result ;
264+ } catch (InterruptedException e ) {
265+ throw Status .INTERNAL
266+ .withDescription ("Unable to extract min and max timestamps from query" )
267+ .withCause (e )
268+ .asRuntimeException ();
269+ }
270+ }
271+
220272 /**
221273 * {@inheritDoc}
222274 */
@@ -234,22 +286,30 @@ public GetJobResponse getJob(GetJobRequest getJobRequest) {
234286 private Table loadEntities (DatasetSource datasetSource ) {
235287 switch (datasetSource .getDatasetSourceCase ()) {
236288 case FILE_SOURCE :
237- String tableName = generateTemporaryTableName ();
238- TableId tableId = TableId .of (projectId , datasetId , tableName );
239- // Currently only avro supported
240- if (datasetSource .getFileSource ().getDataFormat () != DataFormat .DATA_FORMAT_AVRO ) {
241- throw Status .INVALID_ARGUMENT
242- .withDescription ("Invalid file format, only avro supported" )
243- .asRuntimeException ();
244- }
245- LoadJobConfiguration loadJobConfiguration = LoadJobConfiguration .of (tableId ,
246- datasetSource .getFileSource ().getFileUrisList (),
247- FormatOptions .avro ());
248- Job job = bigquery .create (JobInfo .of (loadJobConfiguration ));
249289 try {
290+ String tableName = generateTemporaryTableName ();
291+ log .info ("Loading entity dataset to table {}.{}.{}" , projectId , datasetId , tableName );
292+ TableId tableId = TableId .of (projectId , datasetId , tableName );
293+ // Currently only avro supported
294+ if (datasetSource .getFileSource ().getDataFormat () != DataFormat .DATA_FORMAT_AVRO ) {
295+ throw Status .INVALID_ARGUMENT
296+ .withDescription ("Invalid file format, only avro supported" )
297+ .asRuntimeException ();
298+ }
299+ LoadJobConfiguration loadJobConfiguration = LoadJobConfiguration .of (tableId ,
300+ datasetSource .getFileSource ().getFileUrisList (),
301+ FormatOptions .avro ());
302+ loadJobConfiguration = loadJobConfiguration .toBuilder ()
303+ .setUseAvroLogicalTypes (true )
304+ .build ();
305+ Job job = bigquery .create (JobInfo .of (loadJobConfiguration ));
250306 job .waitFor ();
251- return bigquery .getTable (tableId );
252- } catch (InterruptedException e ) {
307+ Table entityTable = bigquery .getTable (tableId );
308+ if (!entityTable .exists ()) {
309+ throw new RuntimeException ("Unable to create entity dataset table" );
310+ }
311+ return entityTable ;
312+ } catch (Exception e ) {
253313 throw Status .INTERNAL
254314 .withDescription ("Failed to load entity dataset into store" )
255315 .withCause (e )
@@ -264,8 +324,9 @@ private Table loadEntities(DatasetSource datasetSource) {
264324 }
265325
266326 private String generateTemporaryTableName () {
267- String source = String .format ("feast_serving_%d" , System .currentTimeMillis ());
268- UUID uuid = UUID .fromString (source );
269- return uuid .toString ().replaceAll ("-" , "_" );
327+ String source = String .format ("feastserving%d" , System .currentTimeMillis ());
328+ String guid = UUID .nameUUIDFromBytes (source .getBytes ()).toString ();
329+ String suffix = guid .substring (0 , Math .min (guid .length (), 10 )).replaceAll ("-" , "" );
330+ return String .format ("temp_%s" , suffix );
270331 }
271332}
0 commit comments