Skip to content
Snippets Groups Projects
Commit b61909bb authored by anttil's avatar anttil
Browse files

BIS-518 / SP-827: Correct ordering of events, support for events that are not...

BIS-518 / SP-827: Correct ordering of events, support for events that are not mapped to key-value pairs.

SVN: 29680
parent 4ae3037f
No related branches found
No related tags found
No related merge requests found
Showing
with 307 additions and 122 deletions
...@@ -17,41 +17,52 @@ ...@@ -17,41 +17,52 @@
package ch.systemsx.cisd.common.filesystem.control; package ch.systemsx.cisd.common.filesystem.control;
import java.io.File; import java.io.File;
import java.util.Collection; import java.util.ArrayList;
import java.util.HashMap; import java.util.Arrays;
import java.util.Map; import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class FileSystemBasedEventProvider implements IEventProvider /**
* Event source that generates events based on files in given directory. Events are named after the files that are found. Files are deleted after the
* event has been read.
*
* @author anttil
*/
public class ControlDirectoryEventFeed implements IEventFeed
{ {
private final File controlDir; private final File controlDir;
public FileSystemBasedEventProvider(File controlDir) public ControlDirectoryEventFeed(File controlDir)
{ {
this.controlDir = controlDir; this.controlDir = controlDir;
} }
@Override @Override
public Map<String, String> getNewEvents(Collection<String> parameters) public List<String> getNewEvents(IEventFilter filter)
{ {
Map<String, String> map = new HashMap<String, String>(); List<String> events = new ArrayList<String>();
for (File file : controlDir.listFiles()) List<File> files = Arrays.asList(controlDir.listFiles());
{
String fileName = file.getName();
if (file.isFile() && fileName.contains("-"))
{
String key = fileName.substring(0, fileName.lastIndexOf("-"));
if (parameters.contains(key) == false) Collections.sort(files, new Comparator<File>()
{
@Override
public int compare(File f1, File f2)
{ {
continue; return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
} }
});
String value = fileName.substring(fileName.lastIndexOf("-") + 1); for (File file : files)
map.put(key, value); {
String fileName = file.getName();
if (file.isFile() && filter.accepts(fileName))
{
events.add(fileName);
file.delete(); file.delete();
} }
} }
return map; return events;
} }
} }
...@@ -16,13 +16,17 @@ ...@@ -16,13 +16,17 @@
package ch.systemsx.cisd.common.filesystem.control; package ch.systemsx.cisd.common.filesystem.control;
import java.util.Collection; import java.util.ArrayList;
import java.util.HashMap; import java.util.List;
import java.util.Map;
public class DelayingDecorator implements IEventProvider /**
* Decorator that can be used to limit the amount of getNewEvents() calls on the decorated event feed.
*
* @author anttil
*/
public class DelayingDecorator implements IEventFeed
{ {
private final IEventProvider provider; private final IEventFeed eventFeed;
private IClock clock; private IClock clock;
...@@ -30,30 +34,30 @@ public class DelayingDecorator implements IEventProvider ...@@ -30,30 +34,30 @@ public class DelayingDecorator implements IEventProvider
private long lastCall; private long lastCall;
public DelayingDecorator(long interval, IEventProvider provider) public DelayingDecorator(long interval, IEventFeed eventFeed)
{ {
this(interval, provider, new SystemClock()); this(interval, eventFeed, new SystemClock());
} }
DelayingDecorator(long interval, IEventProvider provider, IClock clock) DelayingDecorator(long interval, IEventFeed eventFeed, IClock clock)
{ {
this.provider = provider; this.eventFeed = eventFeed;
this.clock = clock; this.clock = clock;
this.interval = interval; this.interval = interval;
this.lastCall = 0; this.lastCall = 0;
} }
@Override @Override
public Map<String, String> getNewEvents(Collection<String> parameters) public List<String> getNewEvents(IEventFilter filter)
{ {
long currentTime = clock.getTime(); long currentTime = clock.getTime();
if (currentTime - lastCall > interval) if (currentTime - lastCall > interval)
{ {
lastCall = currentTime; lastCall = currentTime;
return provider.getNewEvents(parameters); return eventFeed.getNewEvents(filter);
} else } else
{ {
return new HashMap<String, String>(); return new ArrayList<String>();
} }
} }
} }
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
package ch.systemsx.cisd.common.filesystem.control; package ch.systemsx.cisd.common.filesystem.control;
/** /**
* Abstract clock. The implementation used in production is SystemClock. Tests can use their own implementations if needed.
*
* @author anttil * @author anttil
*/ */
interface IClock interface IClock
......
...@@ -16,13 +16,14 @@ ...@@ -16,13 +16,14 @@
package ch.systemsx.cisd.common.filesystem.control; package ch.systemsx.cisd.common.filesystem.control;
import java.util.Collection; import java.util.List;
import java.util.Map;
/** /**
* Abstract source of events.
*
* @author anttil * @author anttil
*/ */
public interface IEventProvider public interface IEventFeed
{ {
Map<String, String> getNewEvents(Collection<String> parameters); List<String> getNewEvents(IEventFilter filter);
} }
/*
* Copyright 2013 ETH Zuerich, CISD
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.systemsx.cisd.common.filesystem.control;
interface IEventFilter
{
boolean accepts(String value);
}
...@@ -16,9 +16,6 @@ ...@@ -16,9 +16,6 @@
package ch.systemsx.cisd.common.filesystem.control; package ch.systemsx.cisd.common.filesystem.control;
/**
* @author anttil
*/
public interface IValueFilter public interface IValueFilter
{ {
boolean isValid(String value); boolean isValid(String value);
......
...@@ -16,32 +16,34 @@ ...@@ -16,32 +16,34 @@
package ch.systemsx.cisd.common.filesystem.control; package ch.systemsx.cisd.common.filesystem.control;
import java.io.File;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* Collection of key-value pairs that are updated by given event feed. Events have to be of format "key-value", other events are ignored.
*
* @author anttil * @author anttil
*/ */
public class ParameterMap public class ParameterMap
{ {
private final Map<String, String> values; private final Map<String, String> values;
private final Map<String, IValueFilter> filters; private final Map<String, IValueFilter> filters;
private final IEventProvider eventProvider; private final IEventFeed eventFeed;
public ParameterMap(IEventProvider eventProvider) public ParameterMap(IEventFeed eventFeed)
{ {
this.eventProvider = eventProvider; this.eventFeed = eventFeed;
this.values = new HashMap<String, String>(); this.values = new HashMap<String, String>();
this.filters = new HashMap<String, IValueFilter>(); this.filters = new HashMap<String, IValueFilter>();
} }
public void addParameter(String key, String defaultValue) public void addParameter(String key, String defaultValue)
{ {
addParameter(key, defaultValue, dummyFilter()); addParameter(key, defaultValue, acceptAllFilter());
} }
public synchronized void addParameter(String key, String defaultValue, IValueFilter filter) public synchronized void addParameter(String key, String defaultValue, IValueFilter filter)
...@@ -57,48 +59,50 @@ public class ParameterMap ...@@ -57,48 +59,50 @@ public class ParameterMap
public synchronized String get(String key) public synchronized String get(String key)
{ {
Map<String, String> newEvents = eventProvider.getNewEvents(values.keySet()); List<String> events = eventFeed.getNewEvents(eventFilter(values.keySet()));
for (String parameter : newEvents.keySet()) for (String event : events)
{ {
String newValue = newEvents.get(parameter); String parameter = event.substring(0, event.lastIndexOf("-"));
if (filters.get(parameter).isValid(newValue)) String value = event.substring(event.lastIndexOf("-") + 1);
if (filters.get(parameter).isValid(value))
{ {
values.put(parameter, newValue); values.put(parameter, value);
} }
} }
return values.get(key); return values.get(key);
} }
private IValueFilter dummyFilter() private IEventFilter eventFilter(final Set<String> keySet)
{ {
return new IValueFilter() return new IEventFilter()
{ {
@Override @Override
public boolean isValid(String value) public boolean accepts(String event)
{ {
return true; for (String parameter : keySet)
{
if (event.startsWith(parameter + "-"))
{
return true;
}
}
return false;
} }
}; };
} }
public static void main(String args[]) private IValueFilter acceptAllFilter()
{ {
ParameterMap map = new ParameterMap( return new IValueFilter()
new DelayingDecorator(5000,
new FileSystemBasedEventProvider(new File("/tmp/test"))));
map.addParameter("parameter", "100");
while (true)
{
System.out.println(map.get("parameter"));
try
{
Thread.sleep(100);
} catch (InterruptedException ex)
{ {
} @Override
} public boolean isValid(String value)
{
return true;
}
};
} }
} }
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
package ch.systemsx.cisd.common.filesystem.control; package ch.systemsx.cisd.common.filesystem.control;
/** /**
* System clock.
*
* @author anttil * @author anttil
*/ */
class SystemClock implements IClock class SystemClock implements IClock
......
...@@ -19,9 +19,8 @@ package ch.systemsx.cisd.common.filesystem.control; ...@@ -19,9 +19,8 @@ package ch.systemsx.cisd.common.filesystem.control;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Arrays; import java.util.ArrayList;
import java.util.HashMap; import java.util.List;
import java.util.Map;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.jmock.Mockery; import org.jmock.Mockery;
...@@ -37,9 +36,9 @@ public class DelayingDecoratorTest ...@@ -37,9 +36,9 @@ public class DelayingDecoratorTest
private MockClock clock; private MockClock clock;
private IEventProvider provider; private IEventFeed provider;
private IEventProvider decorator; private IEventFeed decorator;
private static final long INTERVAL = 500; private static final long INTERVAL = 500;
...@@ -48,7 +47,7 @@ public class DelayingDecoratorTest ...@@ -48,7 +47,7 @@ public class DelayingDecoratorTest
{ {
clock = new MockClock(System.currentTimeMillis()); clock = new MockClock(System.currentTimeMillis());
context = new Mockery(); context = new Mockery();
provider = context.mock(IEventProvider.class); provider = context.mock(IEventFeed.class);
decorator = new DelayingDecorator(INTERVAL, provider, clock); decorator = new DelayingDecorator(INTERVAL, provider, clock);
} }
...@@ -58,21 +57,35 @@ public class DelayingDecoratorTest ...@@ -58,21 +57,35 @@ public class DelayingDecoratorTest
context.checking(new Expectations() context.checking(new Expectations()
{ {
{ {
Map<String, String> updates = new HashMap<String, String>(); List<String> events = new ArrayList<String>();
updates.put("parameter", "update"); events.add("event-1");
exactly(2).of(provider).getNewEvents(Arrays.asList("parameter")); events.add("event-2");
will(returnValue(updates)); exactly(2).of(provider).getNewEvents(with(Matchers.eventFilterAccepting("event")));
will(returnValue(events));
} }
}); });
assertThat(decorator.getNewEvents(Arrays.asList("parameter")).size(), is(1)); assertThat(decorator.getNewEvents(withName("event")).size(), is(2));
clock.setTime(clock.getTime() + INTERVAL - 1); clock.setTime(clock.getTime() + INTERVAL - 1);
assertThat(decorator.getNewEvents(Arrays.asList("parameter")).size(), is(0)); assertThat(decorator.getNewEvents(withName("event")).size(), is(0));
clock.setTime(clock.getTime() + INTERVAL); clock.setTime(clock.getTime() + INTERVAL);
assertThat(decorator.getNewEvents(Arrays.asList("parameter")).size(), is(1)); assertThat(decorator.getNewEvents(withName("event")).size(), is(2));
context.assertIsSatisfied(); context.assertIsSatisfied();
} }
private IEventFilter withName(final String name)
{
return new IEventFilter()
{
@Override
public boolean accepts(String value)
{
return value.startsWith(name);
}
};
}
} }
...@@ -20,8 +20,7 @@ import static org.hamcrest.CoreMatchers.is; ...@@ -20,8 +20,7 @@ import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import java.io.File; import java.io.File;
import java.util.Arrays; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
...@@ -34,65 +33,99 @@ public class FileSystemBasedEventProviderTest ...@@ -34,65 +33,99 @@ public class FileSystemBasedEventProviderTest
{ {
File controlDir; File controlDir;
private IEventProvider provider; private IEventFeed provider;
@BeforeMethod @BeforeMethod
public void fixture() public void fixture()
{ {
controlDir = new File("/tmp/" + UUID.randomUUID().toString()); controlDir = new File("/tmp/" + UUID.randomUUID().toString());
controlDir.mkdir(); controlDir.mkdir();
provider = new FileSystemBasedEventProvider(controlDir); provider = new ControlDirectoryEventFeed(controlDir);
} }
@Test @Test
public void eventsAreReturnedOnlyOnce() throws Exception public void eventsAreReturnedOnlyOnce() throws Exception
{ {
new File(controlDir, "parameter-x").createNewFile(); createEvent("event");
assertThat(provider.getNewEvents(Arrays.asList("parameter")).isEmpty(), is(false)); assertThat(provider.getNewEvents(eventFilterAccepting("event")).isEmpty(), is(false));
assertThat(provider.getNewEvents(Arrays.asList("parameter")).isEmpty(), is(true)); assertThat(provider.getNewEvents(eventFilterAccepting("event")).isEmpty(), is(true));
}
@Test
public void eventsAreReturnedInCorrectOrder() throws Exception
{
createEvent("this_event", 1000);
createEvent("that_event", 10000);
createEvent("this_event2", 100000);
List<String> events = provider.getNewEvents(allPassingEventFilter());
assertThat(events.size(), is(3));
assertThat(events.get(0), is("this_event"));
assertThat(events.get(1), is("that_event"));
assertThat(events.get(2), is("this_event2"));
} }
@Test @Test
public void filesAreCleaned() throws Exception public void filesAreCleaned() throws Exception
{ {
File event = new File(controlDir, "parameter-x"); File event = new File(controlDir, "event");
event.createNewFile(); event.createNewFile();
provider.getNewEvents(Arrays.asList("parameter")); provider.getNewEvents(eventFilterAccepting("event"));
assertThat(event.exists(), is(false)); assertThat(event.exists(), is(false));
} }
@Test @Test
public void unregisteredfilesAreNotCleaned() throws Exception public void unrelatedFilesAreNotCleaned() throws Exception
{ {
File event = new File(controlDir, "other_parameter-x"); File event = new File(controlDir, "other_event");
event.createNewFile(); event.createNewFile();
Map<String, String> events = provider.getNewEvents(Arrays.asList("parameter")); List<String> events = provider.getNewEvents(eventFilterAccepting("event"));
assertThat(events.isEmpty(), is(true)); assertThat(events.isEmpty(), is(true));
assertThat(event.exists(), is(true)); assertThat(event.exists(), is(true));
} }
@Test private IEventFilter eventFilterAccepting(final String event)
public void keyAndValueAreParsedCorrectlyFromTheFileName() throws Exception
{ {
new File(controlDir, "parameter-x").createNewFile(); return new IEventFilter()
{
Map<String, String> events = provider.getNewEvents(Arrays.asList("parameter"));
@Override
assertThat(events.get("parameter"), is("x")); public boolean accepts(String value)
{
return event.equals(value);
}
};
} }
@Test private IEventFilter allPassingEventFilter()
public void emptyValueWorks() throws Exception
{ {
new File(controlDir, "parameter-").createNewFile(); return new IEventFilter()
{
@Override
public boolean accepts(String value)
{
return true;
}
};
}
Map<String, String> events = provider.getNewEvents(Arrays.asList("parameter")); private void createEvent(String event) throws Exception
{
createEvent(event, System.currentTimeMillis());
}
assertThat(events.get("parameter"), is("")); private void createEvent(String event, long timestamp) throws Exception
{
File f = new File(controlDir, event);
f.createNewFile();
f.setLastModified(timestamp);
} }
} }
/*
* Copyright 2013 ETH Zuerich, CISD
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.systemsx.cisd.common.filesystem.control;
import java.util.UUID;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
/**
* @author anttil
*/
public class Matchers
{
static TypeSafeMatcher<IEventFilter> eventFilterAccepting(final String value)
{
return new TypeSafeMatcher<IEventFilter>()
{
@Override
public void describeTo(Description description)
{
description.appendText("IEventFilter accepting '" + value + "-*'");
}
@Override
public boolean matchesSafely(IEventFilter filter)
{
return filter.accepts(value + "-" + UUID.randomUUID().toString());
}
};
}
}
...@@ -19,12 +19,10 @@ package ch.systemsx.cisd.common.filesystem.control; ...@@ -19,12 +19,10 @@ package ch.systemsx.cisd.common.filesystem.control;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Collection; import java.util.ArrayList;
import java.util.HashMap; import java.util.Arrays;
import java.util.Map; import java.util.List;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.jmock.Expectations; import org.jmock.Expectations;
import org.jmock.Mockery; import org.jmock.Mockery;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
...@@ -35,7 +33,7 @@ import org.testng.annotations.Test; ...@@ -35,7 +33,7 @@ import org.testng.annotations.Test;
*/ */
public class ParameterMapTest public class ParameterMapTest
{ {
IEventProvider eventProvider; IEventFeed eventProvider;
Mockery context; Mockery context;
...@@ -45,7 +43,7 @@ public class ParameterMapTest ...@@ -45,7 +43,7 @@ public class ParameterMapTest
public void fixture() public void fixture()
{ {
context = new Mockery(); context = new Mockery();
eventProvider = context.mock(IEventProvider.class); eventProvider = context.mock(IEventFeed.class);
map = new ParameterMap(eventProvider); map = new ParameterMap(eventProvider);
} }
...@@ -55,8 +53,8 @@ public class ParameterMapTest ...@@ -55,8 +53,8 @@ public class ParameterMapTest
context.checking(new Expectations() context.checking(new Expectations()
{ {
{ {
allowing(eventProvider).getNewEvents(with(collectionContainingExactly("parameter"))); allowing(eventProvider).getNewEvents(with(Matchers.eventFilterAccepting("parameter")));
will(returnValue(new HashMap<String, String>())); will(returnValue(new ArrayList<String>()));
} }
}); });
...@@ -76,7 +74,7 @@ public class ParameterMapTest ...@@ -76,7 +74,7 @@ public class ParameterMapTest
context.checking(new Expectations() context.checking(new Expectations()
{ {
{ {
allowing(eventProvider).getNewEvents(with(collectionContainingExactly("parameter"))); allowing(eventProvider).getNewEvents(with(Matchers.eventFilterAccepting("parameter")));
will(returnValue(getUpdateOn("parameter"))); will(returnValue(getUpdateOn("parameter")));
} }
}); });
...@@ -84,13 +82,27 @@ public class ParameterMapTest ...@@ -84,13 +82,27 @@ public class ParameterMapTest
assertThat(map.get("parameter"), is("updated value")); assertThat(map.get("parameter"), is("updated value"));
} }
@Test
public void emptyValueIsAccepted() throws Exception
{
context.checking(new Expectations()
{
{
allowing(eventProvider).getNewEvents(with(Matchers.eventFilterAccepting("parameter")));
will(returnValue(Arrays.asList("parameter-")));
}
});
map.addParameter("parameter", "default value");
assertThat(map.get("parameter"), is(""));
}
@Test @Test
public void illegalParameterValuesAreNotUpdated() throws Exception public void illegalParameterValuesAreNotUpdated() throws Exception
{ {
context.checking(new Expectations() context.checking(new Expectations()
{ {
{ {
allowing(eventProvider).getNewEvents(with(collectionContainingExactly("parameter"))); allowing(eventProvider).getNewEvents(with(Matchers.eventFilterAccepting("parameter")));
will(returnValue(getUpdateOn("parameter"))); will(returnValue(getUpdateOn("parameter")));
} }
}); });
...@@ -98,12 +110,12 @@ public class ParameterMapTest ...@@ -98,12 +110,12 @@ public class ParameterMapTest
assertThat(map.get("parameter"), is("default value")); assertThat(map.get("parameter"), is("default value"));
} }
private Map<String, String> getUpdateOn(String... keys) private List<String> getUpdateOn(String... keys)
{ {
Map<String, String> updates = new HashMap<String, String>(); List<String> updates = new ArrayList<String>();
for (String key : keys) for (String key : keys)
{ {
updates.put(key, "updated value"); updates.add(key + "-updated value");
} }
return updates; return updates;
} }
...@@ -135,24 +147,4 @@ public class ParameterMapTest ...@@ -135,24 +147,4 @@ public class ParameterMapTest
}; };
} }
private <T> TypeSafeMatcher<Collection<T>> collectionContainingExactly(final T value)
{
return new TypeSafeMatcher<Collection<T>>()
{
@Override
public void describeTo(Description description)
{
description.appendText("Collection containing " + value);
}
@Override
public boolean matchesSafely(Collection<T> collection)
{
return collection.size() == 1 && collection.contains(value);
}
};
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment