Search in sources :

Example 1 with SnapshotsLbRecord

use of org.folio.rest.jooq.tables.records.SnapshotsLbRecord in project mod-source-record-storage by folio-org.

the class RecordDaoImpl method saveRecords.

@Override
public Future<RecordsBatchResponse> saveRecords(RecordCollection recordCollection, String tenantId) {
    Promise<RecordsBatchResponse> promise = Promise.promise();
    Set<UUID> matchedIds = new HashSet<>();
    Set<String> snapshotIds = new HashSet<>();
    Set<String> recordTypes = new HashSet<>();
    List<RecordsLbRecord> dbRecords = new ArrayList<>();
    List<RawRecordsLbRecord> dbRawRecords = new ArrayList<>();
    List<Record2<UUID, JSONB>> dbParsedRecords = new ArrayList<>();
    List<ErrorRecordsLbRecord> dbErrorRecords = new ArrayList<>();
    List<String> errorMessages = new ArrayList<>();
    recordCollection.getRecords().stream().map(RecordDaoUtil::ensureRecordHasId).map(RecordDaoUtil::ensureRecordHasSuppressDiscovery).map(RecordDaoUtil::ensureRecordForeignKeys).forEach(record -> {
        // collect unique matched ids to query to determine generation
        matchedIds.add(UUID.fromString(record.getMatchedId()));
        // make sure only one snapshot id
        snapshotIds.add(record.getSnapshotId());
        if (snapshotIds.size() > 1) {
            throw new BadRequestException("Batch record collection only supports single snapshot");
        }
        if (Objects.nonNull(record.getRecordType())) {
            recordTypes.add(record.getRecordType().name());
        } else {
            throw new BadRequestException(StringUtils.defaultIfEmpty(record.getErrorRecord().getDescription(), String.format("Record with id %s has not record type", record.getId())));
        }
        // make sure only one record type
        if (recordTypes.size() > 1) {
            throw new BadRequestException("Batch record collection only supports single record type");
        }
        // if record has parsed record, validate by attempting format
        if (Objects.nonNull(record.getParsedRecord())) {
            try {
                RecordType recordType = toRecordType(record.getRecordType().name());
                recordType.formatRecord(record);
                Record2<UUID, JSONB> dbParsedRecord = recordType.toDatabaseRecord2(record.getParsedRecord());
                dbParsedRecords.add(dbParsedRecord);
            } catch (Exception e) {
                // create error record and remove from record
                Object content = Objects.nonNull(record.getParsedRecord()) ? record.getParsedRecord().getContent() : null;
                ErrorRecord errorRecord = new ErrorRecord().withId(record.getId()).withDescription(e.getMessage()).withContent(content);
                errorMessages.add(format(INVALID_PARSED_RECORD_MESSAGE_TEMPLATE, record.getId(), e.getMessage()));
                record.withErrorRecord(errorRecord).withParsedRecord(null).withLeaderRecordStatus(null);
            }
        }
        if (Objects.nonNull(record.getRawRecord())) {
            dbRawRecords.add(RawRecordDaoUtil.toDatabaseRawRecord(record.getRawRecord()));
        }
        if (Objects.nonNull(record.getErrorRecord())) {
            dbErrorRecords.add(ErrorRecordDaoUtil.toDatabaseErrorRecord(record.getErrorRecord()));
        }
        dbRecords.add(RecordDaoUtil.toDatabaseRecord(record));
    });
    UUID snapshotId = UUID.fromString(snapshotIds.stream().findFirst().orElseThrow());
    RecordType recordType = toRecordType(recordTypes.stream().findFirst().orElseThrow());
    try (Connection connection = getConnection(tenantId)) {
        DSL.using(connection).transaction(ctx -> {
            DSLContext dsl = DSL.using(ctx);
            // validate snapshot
            Optional<SnapshotsLbRecord> snapshot = DSL.using(ctx).selectFrom(SNAPSHOTS_LB).where(SNAPSHOTS_LB.ID.eq(snapshotId)).fetchOptional();
            if (snapshot.isPresent()) {
                if (Objects.isNull(snapshot.get().getProcessingStartedDate())) {
                    throw new BadRequestException(format(SNAPSHOT_NOT_STARTED_MESSAGE_TEMPLATE, snapshot.get().getStatus()));
                }
            } else {
                throw new NotFoundException(format(SNAPSHOT_NOT_FOUND_TEMPLATE, snapshotId));
            }
            List<UUID> ids = new ArrayList<>();
            Map<UUID, Integer> matchedGenerations = new HashMap<>();
            // lookup latest generation by matched id and committed snapshot updated before current snapshot
            dsl.select(RECORDS_LB.MATCHED_ID, RECORDS_LB.ID, RECORDS_LB.GENERATION).distinctOn(RECORDS_LB.MATCHED_ID).from(RECORDS_LB).innerJoin(SNAPSHOTS_LB).on(RECORDS_LB.SNAPSHOT_ID.eq(SNAPSHOTS_LB.ID)).where(RECORDS_LB.MATCHED_ID.in(matchedIds).and(SNAPSHOTS_LB.STATUS.in(JobExecutionStatus.COMMITTED, JobExecutionStatus.ERROR)).and(SNAPSHOTS_LB.UPDATED_DATE.lessThan(dsl.select(SNAPSHOTS_LB.PROCESSING_STARTED_DATE).from(SNAPSHOTS_LB).where(SNAPSHOTS_LB.ID.eq(snapshotId))))).orderBy(RECORDS_LB.MATCHED_ID.asc(), RECORDS_LB.GENERATION.desc()).fetchStream().forEach(r -> {
                UUID id = r.get(RECORDS_LB.ID);
                UUID matchedId = r.get(RECORDS_LB.MATCHED_ID);
                int generation = r.get(RECORDS_LB.GENERATION);
                ids.add(id);
                matchedGenerations.put(matchedId, generation);
            });
            // update matching records state
            dsl.update(RECORDS_LB).set(RECORDS_LB.STATE, RecordState.OLD).where(RECORDS_LB.ID.in(ids)).execute();
            // batch insert records updating generation if required
            List<LoaderError> recordsLoadingErrors = dsl.loadInto(RECORDS_LB).batchAfter(1000).bulkAfter(500).commitAfter(1000).onErrorAbort().loadRecords(dbRecords.stream().map(record -> {
                Integer generation = matchedGenerations.get(record.getMatchedId());
                if (Objects.nonNull(generation)) {
                    record.setGeneration(generation + 1);
                } else if (Objects.isNull(record.getGeneration())) {
                    record.setGeneration(0);
                }
                return record;
            }).collect(Collectors.toList())).fieldsFromSource().execute().errors();
            recordsLoadingErrors.forEach(error -> {
                if (error.exception().sqlState().equals(UNIQUE_VIOLATION_SQL_STATE)) {
                    throw new DuplicateEventException("SQL Unique constraint violation prevented repeatedly saving the record");
                }
                LOG.warn("Error occurred on batch execution: {}", error.exception().getCause().getMessage());
                LOG.debug("Failed to execute statement from batch: {}", error.query());
            });
            // batch insert raw records
            dsl.loadInto(RAW_RECORDS_LB).batchAfter(250).commitAfter(1000).onDuplicateKeyUpdate().onErrorAbort().loadRecords(dbRawRecords).fieldsFromSource().execute();
            // batch insert parsed records
            recordType.toLoaderOptionsStep(dsl).batchAfter(250).commitAfter(1000).onDuplicateKeyUpdate().onErrorAbort().loadRecords(dbParsedRecords).fieldsFromSource().execute();
            if (!dbErrorRecords.isEmpty()) {
                // batch insert error records
                dsl.loadInto(ERROR_RECORDS_LB).batchAfter(250).commitAfter(1000).onDuplicateKeyUpdate().onErrorAbort().loadRecords(dbErrorRecords).fieldsFromSource().execute();
            }
            promise.complete(new RecordsBatchResponse().withRecords(recordCollection.getRecords()).withTotalRecords(recordCollection.getRecords().size()).withErrorMessages(errorMessages));
        });
    } catch (DuplicateEventException e) {
        LOG.info("Skipped saving records due to duplicate event: {}", e.getMessage());
        promise.fail(e);
    } catch (SQLException | DataAccessException e) {
        LOG.error("Failed to save records", e);
        promise.fail(e);
    }
    return promise.future();
}
Also used : DSL(org.jooq.impl.DSL) RecordsLbRecord(org.folio.rest.jooq.tables.records.RecordsLbRecord) MarcBibCollection(org.folio.rest.jaxrs.model.MarcBibCollection) DSL.field(org.jooq.impl.DSL.field) Autowired(org.springframework.beans.factory.annotation.Autowired) RecordSearchParameters(org.folio.services.RecordSearchParameters) DSL.condition(org.jooq.impl.DSL.condition) StringUtils(org.apache.commons.lang3.StringUtils) SnapshotsLbRecord(org.folio.rest.jooq.tables.records.SnapshotsLbRecord) Condition(org.jooq.Condition) DSL.trueCondition(org.jooq.impl.DSL.trueCondition) RecordsBatchResponse(org.folio.rest.jaxrs.model.RecordsBatchResponse) RawRecordsLbRecord(org.folio.rest.jooq.tables.records.RawRecordsLbRecord) Record2(org.jooq.Record2) Map(java.util.Map) ErrorRecordsLbRecord(org.folio.rest.jooq.tables.records.ErrorRecordsLbRecord) Metadata(org.folio.rest.jaxrs.model.Metadata) ZoneOffset(java.time.ZoneOffset) RAW_RECORDS_LB(org.folio.rest.jooq.Tables.RAW_RECORDS_LB) GenericCompositeFuture(org.folio.okapi.common.GenericCompositeFuture) Set(java.util.Set) SnapshotDaoUtil(org.folio.dao.util.SnapshotDaoUtil) RecordCollection(org.folio.rest.jaxrs.model.RecordCollection) QueryResult(io.github.jklingsporn.vertx.jooq.shared.internal.QueryResult) Logger(org.apache.logging.log4j.Logger) StrSubstitutor(org.apache.commons.lang.text.StrSubstitutor) ParsedRecord(org.folio.rest.jaxrs.model.ParsedRecord) SelectJoinStep(org.jooq.SelectJoinStep) PARSED_RECORD_CONTENT(org.folio.dao.util.ParsedRecordDaoUtil.PARSED_RECORD_CONTENT) MARC_BIB(org.folio.rest.jooq.enums.RecordType.MARC_BIB) ArrayList(java.util.ArrayList) RecordState(org.folio.rest.jooq.enums.RecordState) RECORDS_LB(org.folio.rest.jooq.Tables.RECORDS_LB) UpdateSetFirstStep(org.jooq.UpdateSetFirstStep) SQLException(java.sql.SQLException) RAW_RECORD_CONTENT(org.folio.dao.util.RawRecordDaoUtil.RAW_RECORD_CONTENT) Lists(com.google.common.collect.Lists) Flowable(io.reactivex.Flowable) JobExecutionStatus(org.folio.rest.jooq.enums.JobExecutionStatus) ParsedRecordsBatchResponse(org.folio.rest.jaxrs.model.ParsedRecordsBatchResponse) LoaderError(org.jooq.LoaderError) ParseFieldsResult(org.folio.services.util.parser.ParseFieldsResult) ErrorRecord(org.folio.rest.jaxrs.model.ErrorRecord) UpdateSetMoreStep(org.jooq.UpdateSetMoreStep) Row(io.vertx.sqlclient.Row) RecordDaoUtil.getExternalHrid(org.folio.dao.util.RecordDaoUtil.getExternalHrid) DSL.table(org.jooq.impl.DSL.table) Connection(java.sql.Connection) Table(org.jooq.Table) IdType(org.folio.dao.util.IdType) UpdateConditionStep(org.jooq.UpdateConditionStep) DSLContext(org.jooq.DSLContext) BadRequestException(javax.ws.rs.BadRequestException) SNAPSHOT_NOT_STARTED_MESSAGE_TEMPLATE(org.folio.dao.util.SnapshotDaoUtil.SNAPSHOT_NOT_STARTED_MESSAGE_TEMPLATE) SortOrder(org.jooq.SortOrder) RecordDaoUtil.filterRecordByType(org.folio.dao.util.RecordDaoUtil.filterRecordByType) DSL.name(org.jooq.impl.DSL.name) DuplicateEventException(org.folio.kafka.exception.DuplicateEventException) Name(org.jooq.Name) RecordDaoUtil(org.folio.dao.util.RecordDaoUtil) Collection(java.util.Collection) RecordDaoUtil.filterRecordByState(org.folio.dao.util.RecordDaoUtil.filterRecordByState) Field(org.jooq.Field) UUID(java.util.UUID) ERROR_RECORDS_LB(org.folio.rest.jooq.Tables.ERROR_RECORDS_LB) Future(io.vertx.core.Future) Collectors(java.util.stream.Collectors) RecordDaoUtil.getExternalId(org.folio.dao.util.RecordDaoUtil.getExternalId) NotFoundException(javax.ws.rs.NotFoundException) String.format(java.lang.String.format) Objects(java.util.Objects) List(java.util.List) Optional(java.util.Optional) ErrorRecordDaoUtil(org.folio.dao.util.ErrorRecordDaoUtil) MatchField(org.folio.dao.util.MatchField) RECORD_NOT_FOUND_TEMPLATE(org.folio.dao.util.RecordDaoUtil.RECORD_NOT_FOUND_TEMPLATE) QueryParamUtil.toRecordType(org.folio.rest.util.QueryParamUtil.toRecordType) RawRecord(org.folio.rest.jaxrs.model.RawRecord) SNAPSHOT_NOT_FOUND_TEMPLATE(org.folio.dao.util.SnapshotDaoUtil.SNAPSHOT_NOT_FOUND_TEMPLATE) SNAPSHOTS_LB(org.folio.rest.jooq.Tables.SNAPSHOTS_LB) HashMap(java.util.HashMap) RawRecordDaoUtil(org.folio.dao.util.RawRecordDaoUtil) ParamType(org.jooq.conf.ParamType) Function(java.util.function.Function) HashSet(java.util.HashSet) RecordType(org.folio.dao.util.RecordType) ExternalIdsHolder(org.folio.rest.jaxrs.model.ExternalIdsHolder) DSL.countDistinct(org.jooq.impl.DSL.countDistinct) AdditionalInfo(org.folio.rest.jaxrs.model.AdditionalInfo) SourceRecord(org.folio.rest.jaxrs.model.SourceRecord) DataAccessException(org.jooq.exception.DataAccessException) PgPool(io.vertx.reactivex.pgclient.PgPool) Record(org.folio.rest.jaxrs.model.Record) Promise(io.vertx.core.Promise) ParseLeaderResult(org.folio.services.util.parser.ParseLeaderResult) ReactiveClassicGenericQueryExecutor(io.github.jklingsporn.vertx.jooq.classic.reactivepg.ReactiveClassicGenericQueryExecutor) ParsedRecordDaoUtil(org.folio.dao.util.ParsedRecordDaoUtil) Component(org.springframework.stereotype.Component) ERROR_RECORD_CONTENT(org.folio.dao.util.ErrorRecordDaoUtil.ERROR_RECORD_CONTENT) TypeConnection(org.folio.services.util.TypeConnection) DSL.max(org.jooq.impl.DSL.max) OrderField(org.jooq.OrderField) JSONB(org.jooq.JSONB) LogManager(org.apache.logging.log4j.LogManager) SourceRecordCollection(org.folio.rest.jaxrs.model.SourceRecordCollection) ArrayUtils(org.apache.commons.lang.ArrayUtils) DuplicateEventException(org.folio.kafka.exception.DuplicateEventException) HashMap(java.util.HashMap) SQLException(java.sql.SQLException) ArrayList(java.util.ArrayList) NotFoundException(javax.ws.rs.NotFoundException) RecordsLbRecord(org.folio.rest.jooq.tables.records.RecordsLbRecord) RawRecordsLbRecord(org.folio.rest.jooq.tables.records.RawRecordsLbRecord) ErrorRecordsLbRecord(org.folio.rest.jooq.tables.records.ErrorRecordsLbRecord) QueryParamUtil.toRecordType(org.folio.rest.util.QueryParamUtil.toRecordType) RecordType(org.folio.dao.util.RecordType) LoaderError(org.jooq.LoaderError) UUID(java.util.UUID) Record2(org.jooq.Record2) SnapshotsLbRecord(org.folio.rest.jooq.tables.records.SnapshotsLbRecord) DataAccessException(org.jooq.exception.DataAccessException) HashSet(java.util.HashSet) JSONB(org.jooq.JSONB) Connection(java.sql.Connection) TypeConnection(org.folio.services.util.TypeConnection) DSLContext(org.jooq.DSLContext) RawRecordsLbRecord(org.folio.rest.jooq.tables.records.RawRecordsLbRecord) SQLException(java.sql.SQLException) BadRequestException(javax.ws.rs.BadRequestException) DuplicateEventException(org.folio.kafka.exception.DuplicateEventException) NotFoundException(javax.ws.rs.NotFoundException) DataAccessException(org.jooq.exception.DataAccessException) RecordsBatchResponse(org.folio.rest.jaxrs.model.RecordsBatchResponse) ParsedRecordsBatchResponse(org.folio.rest.jaxrs.model.ParsedRecordsBatchResponse) ErrorRecordsLbRecord(org.folio.rest.jooq.tables.records.ErrorRecordsLbRecord) BadRequestException(javax.ws.rs.BadRequestException) RecordDaoUtil(org.folio.dao.util.RecordDaoUtil) ErrorRecordDaoUtil(org.folio.dao.util.ErrorRecordDaoUtil) RawRecordDaoUtil(org.folio.dao.util.RawRecordDaoUtil) ParsedRecordDaoUtil(org.folio.dao.util.ParsedRecordDaoUtil) ErrorRecord(org.folio.rest.jaxrs.model.ErrorRecord)

Aggregations

Lists (com.google.common.collect.Lists)1 ReactiveClassicGenericQueryExecutor (io.github.jklingsporn.vertx.jooq.classic.reactivepg.ReactiveClassicGenericQueryExecutor)1 QueryResult (io.github.jklingsporn.vertx.jooq.shared.internal.QueryResult)1 Flowable (io.reactivex.Flowable)1 Future (io.vertx.core.Future)1 Promise (io.vertx.core.Promise)1 PgPool (io.vertx.reactivex.pgclient.PgPool)1 Row (io.vertx.sqlclient.Row)1 String.format (java.lang.String.format)1 Connection (java.sql.Connection)1 SQLException (java.sql.SQLException)1 ZoneOffset (java.time.ZoneOffset)1 ArrayList (java.util.ArrayList)1 Collection (java.util.Collection)1 HashMap (java.util.HashMap)1 HashSet (java.util.HashSet)1 List (java.util.List)1 Map (java.util.Map)1 Objects (java.util.Objects)1 Optional (java.util.Optional)1