Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TilesetCache interface instead of java.util.Map<String, TileSet> #3117

Merged
merged 14 commits into from
Aug 30, 2021

Conversation

sergsavchuk
Copy link
Contributor

Hello again!
I submitted a pull request a while ago. And you left a nice suggestion in the comment. I soon implemented this, but forgot to submit a pull request, so here I am :)

Here is an example of interface implementation:

public class FutureTilesetCache implements TilesetCache
{
    private final Logger log = LoggerFactory.getLogger(FutureTilesetCache.class);

    private final Map<String, CompletableFuture<TileSet>> cachedTilesets = new ConcurrentHashMap<>();

    @Override
    public synchronized boolean needToLoadTileset(String tilesetName)
    {
        if (!cachedTilesets.containsKey(tilesetName))
        {
            log.info("Need to load " + tilesetName);
            cachedTilesets.put(tilesetName, new CompletableFuture<>());
            return true;
        }

        return false;
    }

    @Override
    public void tilesetLoadingFinished(TileSet loadedTileset)
    {
        if (cachedTilesets.get(loadedTileset.getName()) == null)
        {
            throw new RuntimeException("Use needToLoadTileset method before loading tileset");
        }
        else if (cachedTilesets.get(loadedTileset.getName()).isDone())
        {
            throw new RuntimeException("Shouldn't be called more than once");
        }

        log.info("Loaded " + loadedTileset.getName());
        cachedTilesets.get(loadedTileset.getName()).complete(loadedTileset);
    }

    @Override
    public TileSet getTileset(String tilesetName)
    {
        try
        {
            return cachedTilesets.get(tilesetName).get();
        }
        catch (InterruptedException | ExecutionException e)
        {
            throw new RuntimeException("Something went wrong", e);
        }
        catch (NullPointerException nullPointerException)
        {
            return null;
        }
    }
}

Copy link
Member

@bjorn bjorn left a comment

Choose a reason for hiding this comment

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

Thanks a lot for making the pull request. Better late than never! :-)

I left one inline comment regarding a thought I had about the suggested caching API.

cachedTilesets.put(name, set);
} else {
set = new TileSet();
return tilesetCache.getTileset(name);
Copy link
Member

@bjorn bjorn Aug 24, 2021

Choose a reason for hiding this comment

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

Would it be reasonable to simplify this API so that the cache API is just getTileset(name, lambda), so that it would call the lambda function to load the tileset when it does not exist in the cache yet? I'm not sure what lambda functions would look like in Java, maybe:

TileSet tileSet = tilesetCache.getTileset(name, (Node t, String name) -> processTileset(t, name))

(and the lambda doesn't really need to capture the name, since it could be read again from the Node)

Or are there some downsides to this?

Copy link
Member

Choose a reason for hiding this comment

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

Or, I guess this should actually look more like:

TileSet tileSet = tilesetCache.getTileset(name, () -> processTileset(t, name))

Since the t and name are local and can't be passed in again as parameters to the lambda. But I don't know how capturing of local values works in Java.

@sergsavchuk
Copy link
Contributor Author

Now interface can be implemented something like this:

public class FutureTilesetCache implements TilesetCache
{
    private final Logger log = LoggerFactory.getLogger(FutureTilesetCache.class);

    private final Map<String, CompletableFuture<TileSet>> cachedTilesets = new ConcurrentHashMap<>();

    @Override
    public TileSet getTileset(String tilesetName, TilesetLoader tilesetLoader)
    {
        try
        {
            if (!cachedTilesets.containsKey(tilesetName))
            {
                if (tilesetLoader == null)
                {
                    return null;
                }

                cachedTilesets.put(tilesetName, new CompletableFuture<>());
                cachedTilesets.get(tilesetName).complete(tilesetLoader.load());
            }

            return
        catch (Exception e)
        {
            throw new RuntimeException("Something went wrong", e);
        }
    }
}

Copy link
Member

@bjorn bjorn left a comment

Choose a reason for hiding this comment

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

Are you sure the implementation of the interface works like that? Since the getTileset function is not synchronized, there could be multiple threads determining that the cache does not contain a tileset with the given name that will then call tilesetLoader.load(), or not?

I've left two small inline comments.

final int tileHeight = getAttribute(t, "tileheight", map != null ? map.getTileHeight() : 0);
final int tileSpacing = getAttribute(t, "spacing", 0);
final int tileMargin = getAttribute(t, "margin", 0);

final String name = getAttributeValue(t, "name");
Copy link
Member

Choose a reason for hiding this comment

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

Now this name variable can be moved into the tilesetCache != null block, since it's not used otherwise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

cachedTilesets = new HashMap<>();
}
if (tilesetCache != null) {
return tilesetCache.getTileset(name, () -> processTileset(t));
Copy link
Member

Choose a reason for hiding this comment

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

How does this bare lambda function turn into a class that implements a load function? Is that just Java magic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it's Java magic 🪄 .
In Java lambdas are simply implementations of single function interfaces.

@sergsavchuk
Copy link
Contributor Author

Yes, you are right, the function itself should be synchronized to avoid synchro problems:

public synchronized TileSet getTileset(String tilesetName, TilesetLoader tilesetLoader)

@bjorn
Copy link
Member

bjorn commented Aug 29, 2021

Yes, you are right, the function itself should be synchronized to avoid synchro problems:

Except then the whole loading of the tileset is synchronized, so any one tileset getting loaded would block all threads requesting a tileset, right?

@sergsavchuk
Copy link
Contributor Author

Yes, you are right, the function itself should be synchronized to avoid synchro problems:

Except then the whole loading of the tileset is synchronized, so any one tileset getting loaded would block all threads requesting a tileset, right?

Yes, this can be solved as in the previous version of the cache:

public class FutureTilesetCache implements TilesetCache
{
    private final Logger log = LoggerFactory.getLogger(FutureTilesetCache.class);

    private final Map<String, CompletableFuture<TileSet>> cachedTilesets = new ConcurrentHashMap<>();

    public synchronized boolean needToLoadTileset(String tilesetName)
    {
        if (!cachedTilesets.containsKey(tilesetName))
        {
            log.info("Need to load " + tilesetName);
            cachedTilesets.put(tilesetName, new CompletableFuture<>());
            return true;
        }
  
        return false;
    }

    @Override
    public TileSet getTileset(String tilesetName, TilesetLoader tilesetLoader)
    {
        try
        {
            if (tilesetLoader != null && needToLoadTileset(tilesetName))
            {
                cachedTilesets.get(tilesetName).complete(tilesetLoader.load());
            }

            return cachedTilesets.get(tilesetName).get();
        }
        catch (Exception e)
        {
            throw new RuntimeException("Something went wrong", e);
        }
    }
}

@bjorn bjorn merged commit 4e0f56e into mapeditor:master Aug 30, 2021
@bjorn
Copy link
Member

bjorn commented Aug 30, 2021

Yes, this can be solved as in the previous version of the cache:

Alright, then the interface is fine as it is! Thanks a lot for making this nice improvement! :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants