Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
702b4d0
test: add scheduled repair concurrency scenario for DatacenterAware m…
0rlych1kk4 Feb 21, 2026
6849741
test: harden ConfigRefresher Awaitility timeouts
0rlych1kk4 Feb 22, 2026
0f6120b
test: make TestScheduleManager idle status deterministic (avoid race)
0rlych1kk4 Feb 22, 2026
8e016db
test: only mount custom schedule config when overrides are requested
0rlych1kk4 Mar 14, 2026
553e546
test: avoid modifying global scheduler frequency (prevent cross-test …
0rlych1kk4 Apr 3, 2026
56f08c1
style: format ecc_config.py with black
0rlych1kk4 Apr 3, 2026
fa10177
test: always mount schedule.yaml and apply overrides only when specified
0rlych1kk4 Apr 3, 2026
e32f8bc
fix: preserve upstream schedule behavior when YAML is empty
0rlych1kk4 Apr 3, 2026
9e6df44
test: harden config refresher and schedule manager determinism
0rlych1kk4 Apr 19, 2026
b38cdf8
chore: apply license headers
0rlych1kk4 Apr 19, 2026
e52cc14
test: increase Cassandra startup timeout for Python integration
0rlych1kk4 Apr 19, 2026
6b3eac4
fix: prevent continuous rescheduling after successful execution
0rlych1kk4 Apr 19, 2026
c2e0ee6
fix: remove invalid exec-maven-plugin parameters
0rlych1kk4 Apr 19, 2026
1564725
Revert "fix: remove invalid exec-maven-plugin parameters"
0rlych1kk4 Apr 19, 2026
f7cb33a
Revert "fix: prevent continuous rescheduling after successful execution"
0rlych1kk4 Apr 19, 2026
74ce536
test: harden python integration setup and OpenAPI fetch retries
0rlych1kk4 Apr 20, 2026
015e639
fix: remove invalid exec-maven-plugin redirect parameters
0rlych1kk4 Apr 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

import java.io.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
Expand All @@ -44,10 +48,20 @@ public void testGetFileContent() throws Exception
configRefresher.watch(file.toPath(), () -> reference.set(readFileContent(file)));

writeToFile(file, "some content");
await().atMost(5, TimeUnit.SECONDS).until(() -> "some content".equals(reference.get()));

// Fix: use a slightly more tolerant poll interval/timeout for CI.
await()
.pollInterval(50, TimeUnit.MILLISECONDS)
.atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> assertThat(reference.get()).isEqualTo("some content"));

writeToFile(file, "some new content");
await().atMost(5, TimeUnit.SECONDS).until(() -> "some new content".equals(reference.get()));

// Fix: remove the brittle / contradictory extra wait and assert only the final expected content.
await()
.pollInterval(50, TimeUnit.MILLISECONDS)
.atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> assertThat(reference.get()).isEqualTo("some new content"));
}
}

Expand All @@ -57,12 +71,15 @@ public void testRunnableThrowsAtFirstUpdate() throws Exception
File file = temporaryFolder.newFile();

AtomicBoolean shouldThrow = new AtomicBoolean(true);
AtomicInteger callbackAttempts = new AtomicInteger(0);

try (ConfigRefresher configRefresher = new ConfigRefresher(temporaryFolder.getRoot().toPath()))
{
AtomicReference<String> reference = new AtomicReference<>(readFileContent(file));

configRefresher.watch(file.toPath(), () -> {
callbackAttempts.incrementAndGet();

if (shouldThrow.get())
{
throw new NullPointerException();
Expand All @@ -73,14 +90,24 @@ public void testRunnableThrowsAtFirstUpdate() throws Exception

writeToFile(file, "some content");

Thread.sleep(100);
// Fix: replace Thread.sleep(...) with deterministic waiting for the first callback attempt.
await()
.pollInterval(50, TimeUnit.MILLISECONDS)
.atMost(10, TimeUnit.SECONDS)
.until(() -> callbackAttempts.get() >= 1);

assertThat(reference).hasValue("");
// Fix: assert that the failing callback did not update the reference.
assertThat(reference.get()).isEqualTo("");

shouldThrow.set(false);

writeToFile(file, "some new content");
await().atMost(5, TimeUnit.SECONDS).until(() -> "some new content".equals(reference.get()));

// Fix: resolved merge conflict and hardened Awaitility timing for CI.
await()
.pollInterval(50, TimeUnit.MILLISECONDS)
.atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> assertThat(reference.get()).isEqualTo("some new content"));
}
}

Expand All @@ -95,7 +122,7 @@ private void writeToFile(File file, String content) throws IOException
private String readFileContent(File file)
{
try (FileReader fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader))
BufferedReader bufferedReader = new BufferedReader(fileReader))
{
StringBuilder result = new StringBuilder();
String line;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018 Telefonaktiebolaget LM Ericsson
* Copyright 2024 Telefonaktiebolaget LM Ericsson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,6 +15,7 @@
package com.ericsson.bss.cassandra.ecchronos.core.scheduling;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyMap;
Expand All @@ -24,6 +25,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
Expand All @@ -42,7 +44,7 @@

import com.ericsson.bss.cassandra.ecchronos.core.exceptions.LockException;

@RunWith (MockitoJUnitRunner.Silent.class)
@RunWith(MockitoJUnitRunner.Silent.class)
public class TestScheduleManager
{
@Mock
Expand Down Expand Up @@ -149,7 +151,7 @@ public void testRunningOneJobWithThrowingLock() throws LockException
assertThat(myScheduler.getQueueSize()).isEqualTo(1);
}

@Test (timeout = 2000L)
@Test(timeout = 5000L)
public void testRunningTwoJobsInParallelShouldFail() throws InterruptedException
{
CountDownLatch job1Latch = new CountDownLatch(1);
Expand All @@ -159,17 +161,24 @@ public void testRunningTwoJobsInParallelShouldFail() throws InterruptedException
myScheduler.schedule(job1);
myScheduler.schedule(job2);

new Thread(() -> myScheduler.run()).start();
new Thread(() -> myScheduler.run()).start();
Thread firstRunner = startSchedulerThread();
Thread secondRunner = startSchedulerThread();

// Fix: deterministic wait instead of manual spin/sleep loop.
waitForJobStarted(job1);
assertThat(job2.hasStarted()).isFalse();

job1Latch.countDown();

// Fix: deterministic completion wait.
waitForJobFinished(job1);

assertThat(job1.hasRun()).isTrue();
assertThat(job2.hasRun()).isFalse();
assertThat(myScheduler.getQueueSize()).isEqualTo(2);

firstRunner.join(1000L);
secondRunner.join(1000L);
}

@Test
Expand Down Expand Up @@ -226,14 +235,14 @@ public void testThreeTasksOneThrowing() throws LockException
verify(myLockFactory, times(3)).tryLock(any(), anyString(), anyInt(), anyMap());
}

@Test (timeout = 2000L)
@Test(timeout = 5000L)
public void testDescheduleRunningJob() throws InterruptedException
{
CountDownLatch jobCdl = new CountDownLatch(1);
TestJob job = new TestJob(ScheduledJob.Priority.HIGH, jobCdl);
myScheduler.schedule(job);

new Thread(() -> myScheduler.run()).start();
Thread schedulerThread = startSchedulerThread();

waitForJobStarted(job);
myScheduler.deschedule(job);
Expand All @@ -242,10 +251,12 @@ public void testDescheduleRunningJob() throws InterruptedException

assertThat(job.hasRun()).isTrue();
assertThat(myScheduler.getQueueSize()).isEqualTo(0);

schedulerThread.join(1000L);
}

@Test
public void testGetCurrentJobStatus() throws InterruptedException
public void testGetCurrentJobStatus()
{
CountDownLatch latch = new CountDownLatch(1);
UUID jobId = UUID.randomUUID();
Expand All @@ -256,15 +267,36 @@ public void testGetCurrentJobStatus() throws InterruptedException
.build(),
jobId,
latch);

myScheduler.schedule(testJob);
new Thread(() -> myScheduler.run()).start();
Thread.sleep(50);
assertThat(myScheduler.getCurrentJobStatus()).isEqualTo(jobId.toString());

Thread schedulerThread = startSchedulerThread();

// Fix: replace brittle Thread.sleep(50) with Awaitility.
await()
.pollInterval(Duration.ofMillis(25))
.atMost(Duration.ofSeconds(5))
.untilAsserted(() -> assertThat(myScheduler.getCurrentJobStatus()).isEqualTo(jobId.toString()));

latch.countDown();

await()
.pollInterval(Duration.ofMillis(25))
.atMost(Duration.ofSeconds(5))
.untilAsserted(() -> assertThat(myScheduler.getCurrentJobStatus()).isEqualTo(""));

try
{
schedulerThread.join(1000L);
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}
}

@Test
public void testGetCurrentJobStatusNoRunning() throws InterruptedException
public void testGetCurrentJobStatusNoRunning()
{
CountDownLatch latch = new CountDownLatch(1);
UUID jobId = UUID.randomUUID();
Expand All @@ -276,24 +308,35 @@ public void testGetCurrentJobStatusNoRunning() throws InterruptedException
jobId,
latch);
myScheduler.schedule(testJob);
new Thread(() -> myScheduler.run()).start();

// Fix: do not start the scheduler here.
// This keeps the test deterministic and verifies the idle-state contract only.
assertThat(myScheduler.getCurrentJobStatus()).isEqualTo("");
latch.countDown();
}
private void waitForJobStarted(TestJob job) throws InterruptedException

private Thread startSchedulerThread()
{
while(!job.hasStarted())
{
Thread.sleep(10);
}
Thread thread = new Thread(() -> myScheduler.run());
thread.setDaemon(true);
thread.start();
return thread;
}

private void waitForJobFinished(TestJob job) throws InterruptedException
private void waitForJobStarted(TestJob job)
{
while(!job.hasRun())
{
Thread.sleep(10);
}
await()
.pollInterval(Duration.ofMillis(25))
.atMost(Duration.ofSeconds(5))
.until(job::hasStarted);
}

private void waitForJobFinished(TestJob job)
{
await()
.pollInterval(Duration.ofMillis(25))
.atMost(Duration.ofSeconds(5))
.until(job::hasRun);
}

private class TestJob extends ScheduledJob
Expand All @@ -305,7 +348,6 @@ private class TestJob extends ScheduledJob
private final int numTasks;
private final Runnable onCompletion;


public TestJob(Priority priority, CountDownLatch cdl)
{
this(priority, cdl, 1, () -> {});
Expand All @@ -328,7 +370,7 @@ public TestJob(Priority priority, CountDownLatch cdl, int numTasks, Runnable onC
super(new ConfigurationBuilder().withPriority(priority).withRunInterval(1, TimeUnit.SECONDS).build());
this.numTasks = numTasks;
this.onCompletion = onCompletion;
countDownLatch = cdl;
this.countDownLatch = cdl;
}

public int getTaskRuns()
Expand Down Expand Up @@ -381,7 +423,8 @@ public boolean execute()
}
catch (InterruptedException e)
{
// Intentionally left empty
Thread.currentThread().interrupt();
return false;
}
onCompletion.run();
taskRuns.incrementAndGet();
Expand All @@ -394,23 +437,28 @@ public boolean execute()
public class TestScheduledJob extends ScheduledJob
{
private final CountDownLatch taskCompletionLatch;

public TestScheduledJob(Configuration configuration, UUID id, CountDownLatch taskCompletionLatch)
{
super(configuration, id);
this.taskCompletionLatch = taskCompletionLatch;
}

@Override
public Iterator<ScheduledTask> iterator()
{
return Collections.<ScheduledTask> singleton(new ControllableTask(taskCompletionLatch)).iterator();
return Collections.<ScheduledTask>singleton(new ControllableTask(taskCompletionLatch)).iterator();
}

class ControllableTask extends ScheduledTask
{
private final CountDownLatch latch;

public ControllableTask(CountDownLatch latch)
{
this.latch = latch;
}

@Override
public boolean execute()
{
Expand Down
8 changes: 1 addition & 7 deletions ecchronos-binary/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,6 @@
</execution>
</executions>
</plugin>


</plugins>
</build>

Expand Down Expand Up @@ -287,7 +285,7 @@
<configuration>
<executable>python</executable>
<arguments>
<argument>-m</argument>
<argument>-m</argument>
<argument>pytest</argument>
<argument>test_ecchronos.py</argument>
<argument>-s</argument>
Expand All @@ -302,8 +300,6 @@
<BASE_DIR>${project.basedir}</BASE_DIR>
<CASSANDRA_VERSION>${it.cassandra.version}</CASSANDRA_VERSION>
</environmentVariables>
<redirectOutput>true</redirectOutput>
<redirectErrorStream>true</redirectErrorStream>
</configuration>
</execution>
</executions>
Expand Down Expand Up @@ -394,8 +390,6 @@
<BASE_DIR>${project.basedir}</BASE_DIR>
<CASSANDRA_VERSION>${it.cassandra.version}</CASSANDRA_VERSION>
</environmentVariables>
<redirectOutput>true</redirectOutput>
<redirectErrorStream>true</redirectErrorStream>
</configuration>
</execution>
</executions>
Expand Down
4 changes: 3 additions & 1 deletion ecchronos-binary/src/test/bin/cass_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
import logging
import subprocess

DEFAULT_WAIT_TIME_IN_SECS = 60
# Fix: increase cluster startup wait time for slower CI environments,
# especially for 4-node Cassandra 5.x startup in GitHub Actions.
DEFAULT_WAIT_TIME_IN_SECS = 120

COMPOSE_FILE_NAME = "docker-compose.yml"
CASSANDRA_SEED_DC1_RC1_ND1 = "cassandra-seed-dc1-rack1-node1"
Expand Down
Loading
Loading