Search code examples
javaspringsetmulti-tenant

java - Why contents of Set are changed unintentionally in the background


In a spring boot microservice, I'm having a Set of Wrapper Objects in my service class. This set represents subscribers, where the wrapper object includes all configurations needed by the subscriber's daos and http client, and this set is regularly synced to check for new subscribers or tenants that unsubscribed. 2 issues at least I could spot at the moment:

  • Whenever adding new subscribers, logging one of the Wrapper Objects' parameters shows them all the same value, although other parameters of object are correct, and also functionality based on faulty printed parameter is also working well. The logging is only for debugging reasons, but for every function applied on the tenant (Wrapper Object) and logging the tenantId, the same tenantId is always logged. TenantId is a part of the Configuration parameter of the Wrapper that becomes similar for all objects in the Set, unlike other parameters in the wrapper object.
  • Whenever a tenant unsubscribes and removing the tenants related instance in the set, on next sync, the set is seen to still contain the tenant and goes to removing again.

The issues seem very simple, but I have no idea where could the issue be, and if they actually are related and may cause more issues.

@Service
public class Service {

    private final Configuration configuration;
    private final ApplicationDao appDao;
    
    private static final HashSet<ConfigurationWrapper> configurations = new HashSet<ConfigurationWrapper>();

    @Autowired
    public Service(Configuration configuration, ApplicationDao appDao) {
        this.configuration = configuration;
        this.appDao = appDao;
        
        try {this.scheduledSubscriptionsSync();
        } catch (IOException | HttpClientException e) {}
    }

    @Scheduled(cron = "0 0 0/1 * * ?")
    private void scheduledSubscriptionsSync() throws IOException, HttpClientException {
        List<User> subs = appDao.getAllSubscriptions();
        addNewSubs(subs);
        removeSubs(subs);
    }

    private void addNewSubs(List<User> subs) {
        for (User sub : subs) {
            Configuration config = configuration;
            config.setUserName(sub.getName());
            config.setPassword(sub.getPassword());
            config.setTenantid(sub.getTenant());
            ConfigurationWrapper newSubConfig = new ConfigurationWrapper(config, sub);
            boolean isNewSubConfigAdded = configurations.add(newSubConfig);
            if (isNewSubConfigAdded) { 
                LOGGER.info("New subscription found: {}", sub.toString());
                int counter = 0;
                for (ConfigurationWrapper configuration : Service.configurations) {
                    LOGGER.debug("!DEBUG! BOOTSTRAPSERVICE - PRINTING HASHSET AFTER ADD: " + configuration.toString() + ", POSITION: " + counter);
                    counter++;
                }
            }
        }
    }
    
    private void removeSubs(List<User> subs) {
        for (ConfigurationWrapper configuration : Service.configurations) {
            if (!subs.contains(configuration.getSubscription())) {
                configurations.remove(configuration);
                LOGGER.info("Subscription removed: {}", configuration.getSubscription().toString());
            }
        }
    }

    public static HashSet<ConfigurationWrapper> getConfigurations() {
        return configurations;
    }
}

I tried changing the data structure of the set, between arrayLists and HashSets, and also making the Set static to avoid expired copies of the set, but still the issues remain.

public class ConfigurationWrapper {
    
    private Configuration configuration;
    private final HttpClient httpClient;
    private final Daos daos;
    private final Timestamps timestamps;
    private final User subscription;

    public ConfigurationWrapper(Configuration configuration, User subscription) {
        this.configuration = configuration;
        this.httpClient = new HttpClient(configuration, CredentialsProviderFactory.getConfigCredentials(configuration));
        this.daos = new Daos(configuration, this.httpClient);
        this.timestamps = new Timestamps();
        this.subscription = subscription;
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public HttpClient getHttpClient() {
        return httpClient;
    }

    public Daos getDaos() {
        return daos;
    }

    public Timestamps getTimestamps() {
        return timestamps;
    }

    public User getSubscription() {
        return subscription;
    }

    @Override
    public String toString() {
        return "ConfigurationWrapper [configuration="
                + configuration + ", subscription=" + subscription + "]";
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(configuration, subscription);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ConfigurationWrapper other = (ConfigurationWrapper) obj;
        return Objects.equals(configuration, other.configuration)
                && Objects.equals(subscription, other.subscription);
    }
}

Solution

  • Problem solved!

    For the first issue, I was using a single Configuration object to create all ConfigurationWrapper objects. This was causing all ConfigurationWrapper objects to have the same Configuration reference. To fix this issue, I had to create a new Configuration object for each ConfigurationWrapper.

    For the second issue, the issue with unsubscribing tenants was related to the way I was comparing and removing them. When I remove a ConfigurationWrapper from the set, I had to also update the iterator or use an iterator specifically designed for this purpose. I fixed it by using Iterator instead of For each.