Skip to content
This repository has been archived by the owner on May 7, 2020. It is now read-only.

ConfigDispatcher: Support for multiple configurations per service #4685

Merged
merged 24 commits into from
Jan 3, 2018

Conversation

triller-telekom
Copy link
Contributor

@triller-telekom triller-telekom commented Dec 6, 2017

Fixes #4370
Fixes #4734
Fixes #4758

Signed-off-by: Stefan Triller stefan.triller@telekom.de

@triller-telekom
Copy link
Contributor Author

I am creating factory configurations based on the "exclusive pid" in the first line of a configuration file, i.e. pid:service.pid#configuration1 will create one service with pid "service.pid" and pid:service.pid#configuration2 will create another service with pid "service.pid" but with a different configuration.

I am "tagging" these configurations in ConfigAdmin via a special property named service.context so I can map the files to a Configuration object in the ConfigAdmin.

Copy link
Contributor

@htreu htreu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see inline comments: The "exclusive PIDs can not be overridden" feature is not existent for exclusive PIDs with context.

lines = lines.subList(1, lines.size());
exclusivePIDMap.setProcessedPID(pid, configFile.getAbsolutePath());
exclusivePIDMap.setProcessedPID(exclusivePID, configFile.getAbsolutePath());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its the right thing to add the full PID including the context here. Although, in line 421 (not part of this PR) the exclusivePID map is checked, whether the given PID is existent there: This should prevent the ConfigDispatcher from overwriting already parsed exclusive configs!
Currently this is possible (there is also a test for this).
See line 409 - 411 for further comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that the current implementation skips the content of the second file?

In https://github.com/eclipse/smarthome/blob/master/bundles/config/org.eclipse.smarthome.config.dispatch/src/main/java/org/eclipse/smarthome/config/dispatch/internal/ConfigDispatcher.java#L351

It creates an empty Properties dictionary and fills it with the content of the second file and then updates the configuration with it.

That's why in this PR I let the duplicate file overwrite the configuration...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I´m sorry! See here: https://github.com/eclipse/smarthome/pull/4685/files#diff-f38a1a84005ec66f7434ebe672d47578R372 for the duplicate check. But it only gives a warning to the log. So overwriting is okay.

configuration = configAdmin.createFactoryConfiguration(pid, null);
Dictionary p = new Properties();
p.put(SERVICE_CONTEXT, context);
configuration.update(p);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only works because the config is not overridden by the check in line 422: When you correct the check there to also take the context into account the SERVICE_CONTEXT property set here will be gone. May be the SERVICE_CONTEXT should be set around the check in line 422 for exclusive PIDs with context, right after creating a blank dictionary for this configuration.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we dont talk about overwriting an existing exclusive config any more I like to stress this issue again: This works only because the check in line 422 checks for the pid which is stripped from the context in this case.
I would like it better to have the new Properties() and p.put(SERVICE_CONTEXT, context) somewhere around the check if we are dealing with an exclusive PID.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have put the creating of the new properties dictionary near the place where the empty properties are created for the non-context cases.

Copy link
Contributor

@htreu htreu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comments and a refactoring proposal.

verifyValueOfConfigurationPropertyWithContext("service.pid#ctx1", "property1", "valueDup");
// ctx2 is parsed as is
verifyValueOfConfigurationPropertyWithContext("service.pid#ctx2", "property1", "value2");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty line

@@ -260,8 +213,8 @@ private void readDefaultConfig() {
}
}

private void readConfigFiles() {
File dir = getSourcePath().toFile();
private void readConfigFiles(Path pathToWatch) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the ConfigDispatcher the path should be named like basePath since the CD does not watch paths any more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought the initial processing of the files in the base config dir might also be a good thing the new ConfigDispatcherFileWatcher could do after activation. It would simply call configDispatcher.processFile in the for loop. Or to make the separation even more fine granular: Let the CDFileWatcher list the files in the base config dir and pass it to a new public void processFiles(List<File> files) on the ConfigDispatcher which does the sorting and processing.
wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the ConfigDispatcher has a strict ordering in its constructor the calls to processOrphanExclusivePIDs() and storeCurrentExclusivePIDList() should be done afterwards in the new processFiles(List<File> files) method.
This will also slim down the c'tor of the ConfigDispatcher :-)

lines = lines.subList(1, lines.size());
exclusivePIDMap.setProcessedPID(pid, configFile.getAbsolutePath());
exclusivePIDMap.setProcessedPID(exclusivePID, configFile.getAbsolutePath());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty line

} catch (IOException e) {
logger.warn("Could not process config file '{}': {}", file.getName(), e.getMessage());
}
processConfigFile(file);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will now recursively process all subdirectories from services. Is this intended? You may just call internalProcessConfigFile here to restore the original behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I had it like that before and thought that this can be done "easier", but I was wrong. Thanks for pointing me to that mistake!

Copy link
Contributor

@htreu htreu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

@htreu
Copy link
Contributor

htreu commented Dec 12, 2017

Please merge here, a follow up PR is waiting for this functionality :-)

@kaikreuzer
Copy link
Contributor

You can easily base your local branch on top of this branch - nothing should block you from working on other PRs. Simply rebase your stuff once this PR is merged.

Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
…Watcher

Separate the processing of file-system events from our ConfigDispatcher
tasks, to simplify tests.

Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Copy link
Contributor

@sjsf sjsf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see inline.

One general question: Is there any particular reason what this mechanism works only for exclusive PIDs?

ConfigDispatcher cd = new ConfigDispatcher(bundleContext, configAdmin);
cd.processConfigFile(new File(absoluteConfigDirectory));

// Modify this file, so that we are sure it is the last modified file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you added this comment here... is touching the file really still necessary, now that the ConfigDispatcher is separated from the file system watcher and you call processConfigFile(...) on it directly?

initialize(configDirectory, servicesDirectory, defaultConfigFileName);
initialize(defaultConfigFileName);

String absoluteConfigDirectory = configDirectory + SEP + servicesDirectory;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it have been worth extracting this block to a method instead of repeating it all over the place?

pid = pidWithContext.split(ConfigDispatcher.SERVICE_CONTEXT_MARKER)[0];
configContext = pidWithContext.split(ConfigDispatcher.SERVICE_CONTEXT_MARKER)[1];
} else {
fail("PID does not have a context");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java throw new IAE(...)

It's nothing you assert about the subject under test but rather a bug in the test code itself.

configs = configAdmin.listConfigurations("(&(service.factoryPid=" + pid + ")("
+ ConfigDispatcher.SERVICE_CONTEXT + "=" + configContext + "))");
} catch (IOException e) {
fail("IOException occured while retrieving configuration for pid " + pidWithContext);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here - these should stay exceptions

if (configs == null) {
return null;
}
// otherwise we should have exactly one configuration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

superfluous comment - it states the obvious and has the risk of getting out-of-sync with the code

*
*/
@Component(immediate = true)
public class ConfigDispatcherFileWatcher extends AbstractWatchService {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good that you factored it out of the config dispatcher and also that its tests do not rely on real file changes anymore. However, you essentially fixed the test which failed on windows by simply dropping them, hence leaving this classes logic completely untested now. Sound like a regression to me. Or did I miss anything?

/**
* Watches file-system events and passes them to our {@code ConfigDispatcher}
*
* @author Stefan Triller - initial contribution
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, you factored it out of another class.

}
}

@Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.STATIC)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STATIC/MANDATORY is the default, hence just @Reference is enough

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my observations at least the IDE does produce different XML in both cases: @Reference only does not put 1..1& static into the XML.

cd.fileRemoved(serviceConfigFile.getAbsolutePath());

Configuration c1 = getConfigurationWithContext("service.pid#ctx1");
// test if configuration with context ctx1 was deleted
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you added a commend and a failure message. Both are best case superfluous by stating the obvious or even worse are harmful to readability by stating the exact opposite of what actually should be asserted here. Please drop them both. Everywhere. (Won't repeat that at every occurrence)

} else if (context != null && exclusivePIDMap.contains(exclusivePID)) {
Dictionary p = new Properties();
p.put(SERVICE_CONTEXT, context);
configuration.update(p);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this line here intended to do in any of these cases?

  • new service configuration as created in line 324
  • an existing service configuration as grabbed in line 316

The Configuration.update() method is the information for the config admin that the config has changed and it will instantiate the new service instance with these (here empty) properties. The same applies to existing ones: they will get modified with empty properties, just to be updated later again with the real values again.

I understand that you just copied the same pattern from below - which doesn't make it any better and should be fixed there too. This explains why all of our services always get instantiated with "null" configurations at first and then get an updated afterwards immediately with the real values... 😄 This created unneeded restarts and re-initializations of all kinds of crap transitively.

Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Notify configadmin only if there is a new configuration

Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
@triller-telekom
Copy link
Contributor Author

One general question: Is there any particular reason what this mechanism works only for exclusive PIDs?

Since the purpose of different contexts is that they hold configurations for different use cases I thought it makes sense to also separate the use cases into different files, each holding one configuration for exactly one use case.

I am not exactly sure on how to properly test the new ConfigDispatcherFileWatcher. Its solely purpose is to react on file-system changes and then delegate the work to the ConfigDispatcher. So all I could check is if the void processWatchEvent(WatchEvent<?> event, Kind<?> kind, Path path) method is called with the right arguments if I put a file on the file-system. But isn't this basic java stuff and we would have other problems if this wouldn't work? Also this is a OGSi service and creating an OSGi test for such simply java build-in functionality is a bit overkill, isn't it? Please correct me if my assumptions are wrong and point me to a solution.

// Modify this file, so that we are sure it is the last modified file

As you added this comment here... is touching the file really still necessary, now that the ConfigDispatcher is separated from the file system watcher and you call processConfigFile(...) on it directly?

Yes, this is necessary because the tests itself look into the file-system to find the last modified file and then compare it against the value in the configuration, see https://github.com/eclipse/smarthome/blob/master/bundles/config/org.eclipse.smarthome.config.dispatch.test/src/test/java/org/eclipse/smarthome/config/dispatch/test/ConfigDispatcherOSGiTest.java#L798-L838

@htreu
Copy link
Contributor

htreu commented Dec 19, 2017

I'd like to add to the ConfigDispatcherFileWatcher test discussion: We could easily make the ConfigDispatcher an OSGi service and attach it to the CDFW using a setter (mandatory, static). Then set up a plain unit test for CDFW and providing a mock ConfigDispatcher. Then simply call the processWatchEvent method while verifying the correct call on the ConfigDispatcher mock. wdyt?

Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
*/
@Component(immediate = true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intended to remove the immediate=true?

@Activate
public void activate(BundleContext bundleContext) {
super.activate();
// configDispatcher = new ConfigDispatcher(bundleContext);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be removed?

@Override
public void deactivate() {
super.deactivate();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove empty line

}

@Activate
public void activate(BundleContext bundleContext) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better override the activate() method without a parameter.

Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
@sjsf sjsf merged commit c73ec3b into eclipse-archived:master Jan 3, 2018
@triller-telekom triller-telekom deleted the serviceFactory branch January 3, 2018 14:30
dgajic pushed a commit to dgajic/smarthome that referenced this pull request Jan 27, 2018
…lipse-archived#4685)

* ConfigDispatcher: Support for multiple configurations per service
* Split ConfigDispatcher into ConfigDispatcher and ConfigDispatcherFileWatcher
* Move initial config processing out of ConfigDispatcher constructor
* Do not accidentally parse subdirectories
* Remove unnecessary update on configuration

Signed-off-by: Stefan Triller <stefan.triller@telekom.de>
@htreu htreu added this to the 0.10.0 milestone Oct 30, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants