Jenkins messes up system time while running Unit tests with usage of RevisionRepository

时间:2018-12-19 11:34:26

标签: java unit-testing jenkins spring-data-jpa hibernate-envers

We are working on revisioning framework for our application by using spring Envers support. We prepared a set of integration tests, which are first inserting data into database and then querying it with some conditions, to check that versioning is working fine. Rows are inserted with milliseconds delays, to ensure, that two rows will not have the same RevisionTimestamp. All tests work fine, when are executed on local environments, but when are moved to Jenkins, they are failing randomly. After investigation we figured it out, that even though inserts are executed sequentially by on thread, sometimes RevisionTimestamp is set backwards. This is due to the fact, that system clock on Jenkins sometimes is moved backwards by unknown force.

Here is an example of log:

2018-12-19 11:02:33.615 [main] INFO  d.e.s.l.u.b.core.TemporalHandler - Just before going into entityJpaRepository.findLastChangeRevision - 2018-12-19T11:02:33.615639 6847516000080345
2018-12-19 11:02:33.377 [main] INFO  d.e.s.l.u.b.core.TemporalHandler - Just after going into entityJpaRepository.findLastChangeRevision - 2018-12-19T11:02:33.377086 6847516058513093

Notice that first timestamp from first log line is higher than first timestamp from second line, but second timestamp from first line is lower than second timestamp from second line.

Log comes from following piece of code:

@Component("temporalHandler")
public class TemporalHandler<T extends Temporal<T, N>, N> implements EntityRevisionsTemporalApi<T, N> {
        private final static transient Logger log = LoggerFactory.getLogger(TemporalHandler.class);
        private static final long serialVersionUID = -7854492585050762415L;

        @PersistenceContext
        @Autowired
        public transient EntityManager em;

        protected transient GenericEntityRepository<?, N> entityJpaRepository;

        /**
         * Setting the entity repository instance dynamically by the consumer component
         *
         * @param entityJpaRepository instance of the entity repository that needs to be
         *                            audited
         */
        public void setEntityJpaRepository(GenericEntityRepository<?, N> entityJpaRepository) {
            this.entityJpaRepository = entityJpaRepository;
        }

        @SuppressWarnings({"unchecked", "hiding"})
        @Override
        public T getEntityLatestRevision(@NotNull N id) {
            try {
                log.info("Just before going into entityJpaRepository.findLastChangeRevision - " + LocalDateTime.now() + " " + System.nanoTime());
                var result = entityJpaRepository.findLastChangeRevision(id);
                log.info("Just after going into entityJpaRepository.findLastChangeRevision - " + LocalDateTime.now() + " " + System.nanoTime());
                return result
                        .map(r -> ((Revision<Integer, T>) r).getEntity())
                        .orElse(null);
            } finally {
                log.info("Just after going into result.map.orElse - " + LocalDateTime.now() + " " + System.nanoTime());
            }

        }
    }

And Generic repository:

@NoRepositoryBean
public interface GenericEntityRepository<T, N> extends RevisionRepository<T, N, Integer>, JpaRepository<T, N> {
}

We are trying to solve this issue for several days already, but with no success. We are 99% sure it's related to something on Jenkins, but we have no idea what could be different there. On local environments both dates from the log are in sync, so as expected, both timestamps from first line are before both timestamps from second.

Maybe someone of you will have a clue, what should we check on Jenkins? Any help will be appreciated.

Thx.

@Update We have some more logs, we know, when time is changed, but still no idea, why this could happen. Checkout last 3 lines of this log:

2018-12-19 18:32:54.853 [main] DEBUG o.h.engine.internal.TwoPhaseLoad - Resolving associations for [org.hibernate.envers.DefaultRevisionEntity#152]
2018-12-19 18:32:54.853 [main] DEBUG o.h.engine.internal.TwoPhaseLoad - Done materializing entity [org.hibernate.envers.DefaultRevisionEntity#152]
2018-12-19 18:32:54.853 [main] TRACE o.h.e.j.internal.JdbcCoordinatorImpl - Starting after statement execution processing [ON_CLOSE]
2018-12-19 18:32:54.853 [main] TRACE o.h.e.i.StatefulPersistenceContext - Initializing non-lazy collections
2018-12-19 18:32:54.853 [main] TRACE o.h.engine.query.spi.QueryPlanCache - Located HQL query plan in cache (select e__ from entity.TestEntity_AUD e__ where e__.originalId.REV.id = (select max(e2__.originalId.REV.id) from entity.TestEntity_AUD e2__ where e2__.originalId.REV.id <= :revision and e__.originalId.id = e2__.originalId.id) and e__.REVTYPE <> :_p0 and e__.originalId.id = :_p1)
2018-12-19 18:32:54.853 [main] TRACE o.h.engine.query.spi.QueryPlanCache - Located HQL query plan in cache (select e__ from entity.TestEntity_AUD e__ where e__.originalId.REV.id = (select max(e2__.originalId.REV.id) from entity.TestEntity_AUD e2__ where e2__.originalId.REV.id <= :revision and e__.originalId.id = e2__.originalId.id) and e__.REVTYPE <> :_p0 and e__.originalId.id = :_p1)
2018-12-19 18:32:54.853 [main] TRACE o.hibernate.engine.internal.Cascade - Processing cascade ACTION_PERSIST_ON_FLUSH for: org.hibernate.envers.DefaultRevisionEntity
2018-12-19 18:32:54.853 [main] TRACE o.hibernate.engine.internal.Cascade - Done processing cascade ACTION_PERSIST_ON_FLUSH for: org.hibernate.envers.DefaultRevisionEntity
2018-12-19 18:32:54.853 [main] TRACE o.h.engine.query.spi.HQLQueryPlan - Find: select e__ from dk.eg.sd.loen.utility.bitemporal.entity.TestEntity_AUD e__ where e__.originalId.REV.id = (select max(e2__.originalId.REV.id) from entity.TestEntity_AUD e2__ where e2__.originalId.REV.id <= :revision and e__.originalId.id = e2__.originalId.id) and e__.REVTYPE <> :_p0 and e__.originalId.id = :_p1
2018-12-19 18:32:54.853 [main] TRACE o.h.engine.spi.QueryParameters - Named parameters: {_p1=1, _p0=DEL, revision=152}
2018-12-19 18:32:54.853 [main] TRACE o.h.e.j.internal.JdbcCoordinatorImpl - Registering last query statement [HikariProxyPreparedStatement@773469572 wrapping prep1068: select testentity0_.id as id1_1_, testentity0_.rev as rev2_1_, testentity0_.revtype as revtype3_1_, testentity0_.revision_system_time as revision4_1_, testentity0_.revision_system_time_mod as revision5_1_, testentity0_.revision_time as revision6_1_, testentity0_.revision_time_mod as revision7_1_, testentity0_.effective_end_time as effectiv8_1_, testentity0_.effective_end_time_mod as effectiv9_1_, testentity0_.effective_start_time as effecti10_1_, testentity0_.effective_start_time_mod as effecti11_1_, testentity0_.employee_id as employe12_1_, testentity0_.employee_id_mod as employe13_1_, testentity0_.first_name as first_n14_1_, testentity0_.first_name_mod as first_n15_1_, testentity0_.last_name as last_na16_1_, testentity0_.last_name_mod as last_na17_1_, testentity0_.salary as salary18_1_, testentity0_.salary_mod as salary_19_1_ from test_entity_aud testentity0_ where testentity0_.rev=(select max(testentity1_.rev) from test_entity_aud testentity1_ where testentity1_.rev<=? and testentity0_.id=testentity1_.id) and testentity0_.revtype<>? and testentity0_.id=?]
2018-12-19 18:32:54.631 [main] DEBUG o.h.engine.internal.TwoPhaseLoad - Resolving associations for [entity.TestEntity_AUD#component[id,REV]{REV=org.hibernate.envers.DefaultRevisionEntity#152, id=1}]
2018-12-19 18:32:54.632 [main] DEBUG o.h.engine.internal.TwoPhaseLoad - Done materializing entity [entity.TestEntity_AUD#component[id,REV]{REV=org.hibernate.envers.DefaultRevisionEntity#152, id=1}]

2 个答案:

答案 0 :(得分:0)

什么是versionTimestamp,它是日期格式,用于什么?

根据共享的信息,这是我的想法:

如果您将时间戳记用作唯一约束或用于排序,则不是最佳做法。

有时在操作系统上时间会倒退。
NTP进程用于同步计算机上的时间。如果此过程在您的一台服务器上不同步,则报告的时间可能比实际时间早。如果有多个服务器在运行,则时间可能会有所不同。所有服务器可能都没有达到毫秒级精度的时钟同步。不能保证跨服务器订购。

有两种时钟:
单调时钟:报告持续时间已过
挂钟:报告现在几点

可能的解决方案:
可能要研究自动递增的ID或类似雪花的版本控制。

增加在连续插入之间的集成测试中估计的时间偏移。

查看是否可以依赖在时间戳列创建的数据库

检查LocalDateTime.now()提供的精度水平

答案 1 :(得分:0)

我只能说与hibernate-envers有直接关系的讲话。我对spring-data-envers封装库的方式或方式没有太多经验,因此我无法深入了解其合同提供的内容或工作方式。

话虽如此,我很愿意在这里与Ankit达成协议,这也正是为什么修订实体表REVINFO就是这样的原因。

REVINFO
------------------------------------
REV      numeric   auto_increment PK
REVTSTMP timestamp 

hibernate-envers生成的所有审计行均具有指向该表结构的外键,每个实体审计表中的REV列均指向该表中的一行。这意味着对于单个交易范围内的所有更改,将仅存在 ONE REVREVTSTMP值。

hibernate-envers要使用任何形式的订单返回实体的审计更改时,我们根本不使用REVTSTMP列,而使用REV列。我们使用REV列是因为它保证是顺序确定的,因为它基于某些数据库生成的构造。无论是身份,auto_increment还是序列支持的对象。

关于其价值,REVTSTMP列仅供参考。它的目的是为用户或应用提供有关何时进行审计更改的某些上下文,但是当要求提供更改的有序依赖性时,我们从不使用该列;相反,我们仅依靠REV

我建议在这里修改测试以依赖REV