poll key images in batches

This commit is contained in:
woodser 2025-05-31 09:10:52 -04:00 committed by woodser
parent 5b04eb17a2
commit 9dd011afc8

View file

@ -28,6 +28,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -42,12 +43,15 @@ public class XmrKeyImagePoller {
private MoneroDaemon daemon; private MoneroDaemon daemon;
private long refreshPeriodMs; private long refreshPeriodMs;
private Object lock = new Object();
private Map<String, Set<String>> keyImageGroups = new HashMap<String, Set<String>>(); private Map<String, Set<String>> keyImageGroups = new HashMap<String, Set<String>>();
private LinkedHashSet<String> keyImagePollQueue = new LinkedHashSet<>();
private Set<XmrKeyImageListener> listeners = new HashSet<XmrKeyImageListener>(); private Set<XmrKeyImageListener> listeners = new HashSet<XmrKeyImageListener>();
private TaskLooper looper; private TaskLooper looper;
private Map<String, MoneroKeyImageSpentStatus> lastStatuses = new HashMap<String, MoneroKeyImageSpentStatus>(); private Map<String, MoneroKeyImageSpentStatus> lastStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
private boolean isPolling = false; private boolean isPolling = false;
private Long lastLogPollErrorTimestamp; private Long lastLogPollErrorTimestamp;
private static final int MAX_POLL_SIZE = 200;
/** /**
* Construct the listener. * Construct the listener.
@ -74,9 +78,11 @@ public class XmrKeyImagePoller {
* @param listener - the listener to add * @param listener - the listener to add
*/ */
public void addListener(XmrKeyImageListener listener) { public void addListener(XmrKeyImageListener listener) {
synchronized (lock) {
listeners.add(listener); listeners.add(listener);
refreshPolling(); refreshPolling();
} }
}
/** /**
* Remove a listener to receive notifications. * Remove a listener to receive notifications.
@ -84,10 +90,12 @@ public class XmrKeyImagePoller {
* @param listener - the listener to remove * @param listener - the listener to remove
*/ */
public void removeListener(XmrKeyImageListener listener) { public void removeListener(XmrKeyImageListener listener) {
synchronized (lock) {
if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered"); if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered");
listeners.remove(listener); listeners.remove(listener);
refreshPolling(); refreshPolling();
} }
}
/** /**
* Set the Monero daemon to fetch key images from. * Set the Monero daemon to fetch key images from.
@ -140,10 +148,11 @@ public class XmrKeyImagePoller {
* @param keyImages - key images to listen to * @param keyImages - key images to listen to
*/ */
public void addKeyImages(Collection<String> keyImages, String groupId) { public void addKeyImages(Collection<String> keyImages, String groupId) {
synchronized (this.keyImageGroups) { synchronized (lock) {
if (!keyImageGroups.containsKey(groupId)) keyImageGroups.put(groupId, new HashSet<String>()); if (!keyImageGroups.containsKey(groupId)) keyImageGroups.put(groupId, new HashSet<String>());
Set<String> keyImagesGroup = keyImageGroups.get(groupId); Set<String> keyImagesGroup = keyImageGroups.get(groupId);
keyImagesGroup.addAll(keyImages); keyImagesGroup.addAll(keyImages);
keyImagePollQueue.addAll(keyImages);
refreshPolling(); refreshPolling();
} }
} }
@ -154,36 +163,34 @@ public class XmrKeyImagePoller {
* @param keyImages - key images to unlisten to * @param keyImages - key images to unlisten to
*/ */
public void removeKeyImages(Collection<String> keyImages, String groupId) { public void removeKeyImages(Collection<String> keyImages, String groupId) {
synchronized (keyImageGroups) { synchronized (lock) {
Set<String> keyImagesGroup = keyImageGroups.get(groupId); Set<String> keyImagesGroup = keyImageGroups.get(groupId);
if (keyImagesGroup == null) return; if (keyImagesGroup == null) return;
keyImagesGroup.removeAll(keyImages); keyImagesGroup.removeAll(keyImages);
if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId); if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId);
Set<String> allKeyImages = getKeyImages(); Set<String> allKeyImages = getKeyImages();
synchronized (lastStatuses) {
for (String keyImage : keyImages) { for (String keyImage : keyImages) {
if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) { if (!allKeyImages.contains(keyImage)) {
keyImagePollQueue.remove(keyImage);
lastStatuses.remove(keyImage); lastStatuses.remove(keyImage);
} }
} }
}
refreshPolling(); refreshPolling();
} }
} }
public void removeKeyImages(String groupId) { public void removeKeyImages(String groupId) {
synchronized (keyImageGroups) { synchronized (lock) {
Set<String> keyImagesGroup = keyImageGroups.get(groupId); Set<String> keyImagesGroup = keyImageGroups.get(groupId);
if (keyImagesGroup == null) return; if (keyImagesGroup == null) return;
keyImageGroups.remove(groupId); keyImageGroups.remove(groupId);
Set<String> allKeyImages = getKeyImages(); Set<String> allKeyImages = getKeyImages();
synchronized (lastStatuses) {
for (String keyImage : keyImagesGroup) { for (String keyImage : keyImagesGroup) {
if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) { if (!allKeyImages.contains(keyImage)) {
keyImagePollQueue.remove(keyImage);
lastStatuses.remove(keyImage); lastStatuses.remove(keyImage);
} }
} }
}
refreshPolling(); refreshPolling();
} }
} }
@ -192,11 +199,10 @@ public class XmrKeyImagePoller {
* Clear the key images which stops polling. * Clear the key images which stops polling.
*/ */
public void clearKeyImages() { public void clearKeyImages() {
synchronized (keyImageGroups) { synchronized (lock) {
keyImageGroups.clear(); keyImageGroups.clear();
synchronized (lastStatuses) { keyImagePollQueue.clear();
lastStatuses.clear(); lastStatuses.clear();
}
refreshPolling(); refreshPolling();
} }
} }
@ -208,7 +214,7 @@ public class XmrKeyImagePoller {
* @return true if the key is spent, false if unspent, null if unknown * @return true if the key is spent, false if unspent, null if unknown
*/ */
public Boolean isSpent(String keyImage) { public Boolean isSpent(String keyImage) {
synchronized (lastStatuses) { synchronized (lock) {
if (!lastStatuses.containsKey(keyImage)) return null; if (!lastStatuses.containsKey(keyImage)) return null;
return XmrKeyImagePoller.isSpent(lastStatuses.get(keyImage)); return XmrKeyImagePoller.isSpent(lastStatuses.get(keyImage));
} }
@ -231,7 +237,7 @@ public class XmrKeyImagePoller {
* @return the last known spent status of the key image * @return the last known spent status of the key image
*/ */
public MoneroKeyImageSpentStatus getLastSpentStatus(String keyImage) { public MoneroKeyImageSpentStatus getLastSpentStatus(String keyImage) {
synchronized (lastStatuses) { synchronized (lock) {
return lastStatuses.get(keyImage); return lastStatuses.get(keyImage);
} }
} }
@ -244,7 +250,7 @@ public class XmrKeyImagePoller {
// fetch spent statuses // fetch spent statuses
List<MoneroKeyImageSpentStatus> spentStatuses = null; List<MoneroKeyImageSpentStatus> spentStatuses = null;
List<String> keyImages = new ArrayList<String>(getKeyImages()); List<String> keyImages = new ArrayList<String>(getNextKeyImageBatch());
try { try {
spentStatuses = keyImages.isEmpty() ? new ArrayList<MoneroKeyImageSpentStatus>() : daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter spentStatuses = keyImages.isEmpty() ? new ArrayList<MoneroKeyImageSpentStatus>() : daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter
} catch (Exception e) { } catch (Exception e) {
@ -257,10 +263,20 @@ public class XmrKeyImagePoller {
return; return;
} }
// collect changed statuses // process spent statuses
Map<String, MoneroKeyImageSpentStatus> changedStatuses = new HashMap<String, MoneroKeyImageSpentStatus>(); Map<String, MoneroKeyImageSpentStatus> changedStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
synchronized (lastStatuses) { synchronized (lock) {
for (int i = 0; i < spentStatuses.size(); i++) { Set<String> allKeyImages = getKeyImages();
for (int i = 0; i < keyImages.size(); i++) {
// skip if key image is removed
if (!allKeyImages.contains(keyImages.get(i))) continue;
// move key image to the end of the queue
keyImagePollQueue.remove(keyImages.get(i));
keyImagePollQueue.add(keyImages.get(i));
// update spent status
if (spentStatuses.get(i) != lastStatuses.get(keyImages.get(i))) { if (spentStatuses.get(i) != lastStatuses.get(keyImages.get(i))) {
lastStatuses.put(keyImages.get(i), spentStatuses.get(i)); lastStatuses.put(keyImages.get(i), spentStatuses.get(i));
changedStatuses.put(keyImages.get(i), spentStatuses.get(i)); changedStatuses.put(keyImages.get(i), spentStatuses.get(i));
@ -270,14 +286,18 @@ public class XmrKeyImagePoller {
// announce changes // announce changes
if (!changedStatuses.isEmpty()) { if (!changedStatuses.isEmpty()) {
for (XmrKeyImageListener listener : new ArrayList<XmrKeyImageListener>(listeners)) { List<XmrKeyImageListener> listeners;
synchronized (lock) {
listeners = new ArrayList<XmrKeyImageListener>(this.listeners);
}
for (XmrKeyImageListener listener : listeners) {
listener.onSpentStatusChanged(changedStatuses); listener.onSpentStatusChanged(changedStatuses);
} }
} }
} }
private void refreshPolling() { private void refreshPolling() {
synchronized (keyImageGroups) { synchronized (lock) {
setIsPolling(!getKeyImages().isEmpty() && listeners.size() > 0); setIsPolling(!getKeyImages().isEmpty() && listeners.size() > 0);
} }
} }
@ -296,11 +316,24 @@ public class XmrKeyImagePoller {
private Set<String> getKeyImages() { private Set<String> getKeyImages() {
Set<String> allKeyImages = new HashSet<String>(); Set<String> allKeyImages = new HashSet<String>();
synchronized (keyImageGroups) { synchronized (lock) {
for (Set<String> keyImagesGroup : keyImageGroups.values()) { for (Set<String> keyImagesGroup : keyImageGroups.values()) {
allKeyImages.addAll(keyImagesGroup); allKeyImages.addAll(keyImagesGroup);
} }
} }
return allKeyImages; return allKeyImages;
} }
private List<String> getNextKeyImageBatch() {
synchronized (lock) {
List<String> keyImageBatch = new ArrayList<>();
int count = 0;
for (String keyImage : keyImagePollQueue) {
if (count >= MAX_POLL_SIZE) break;
keyImageBatch.add(keyImage);
count++;
}
return keyImageBatch;
}
}
} }