0

I have a CXF client configured in my Spring Boot app like so:

    @Bean
    public ConsumerSupportService consumerSupportService() {
        JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean();
        jaxWsProxyFactoryBean.setServiceClass(ConsumerSupportService.class);
        jaxWsProxyFactoryBean.setAddress("https://www.someservice.com/service?wsdl");
        jaxWsProxyFactoryBean.setBindingId(SOAPBinding.SOAP12HTTP_BINDING);
        WSAddressingFeature wsAddressingFeature = new WSAddressingFeature();
        wsAddressingFeature.setAddressingRequired(true);
        jaxWsProxyFactoryBean.getFeatures().add(wsAddressingFeature);

        ConsumerSupportService service =  (ConsumerSupportService) jaxWsProxyFactoryBean.create();

        Client client = ClientProxy.getClient(service);
        AddressingProperties addressingProperties = new AddressingProperties();
        AttributedURIType to = new AttributedURIType();
        to.setValue(applicationProperties.getWex().getServices().getConsumersupport().getTo());
        addressingProperties.setTo(to);
        AttributedURIType action = new AttributedURIType();
        action.setValue("http://serviceaction/SearchConsumer");
        addressingProperties.setAction(action);
        client.getRequestContext().put("javax.xml.ws.addressing.context", addressingProperties);

        setClientTimeout(client);

        return service;
    }

    private void setClientTimeout(Client client) {
        HTTPConduit conduit = (HTTPConduit) client.getConduit();
        HTTPClientPolicy policy = new HTTPClientPolicy();
        policy.setConnectionTimeout(applicationProperties.getWex().getServices().getClient().getConnectionTimeout());
        policy.setReceiveTimeout(applicationProperties.getWex().getServices().getClient().getReceiveTimeout());
        conduit.setClient(policy);
    }

This same service bean is accessed by two different threads in the same application sequence. If I execute this particular sequence 10 times in a row, I will get a connection timeout from the service call at least 3 times. What I'm seeing is:

Caused by: java.io.IOException: Timed out waiting for response to operation {http://theservice.com}SearchConsumer.
    at org.apache.cxf.endpoint.ClientImpl.waitResponse(ClientImpl.java:685) ~[cxf-core-3.2.0.jar:3.2.0]
    at org.apache.cxf.endpoint.ClientImpl.processResult(ClientImpl.java:608) ~[cxf-core-3.2.0.jar:3.2.0]

If I change the sequence such that one of the threads does not call this service, then the error goes away. So, it seems like there's some sort of a race condition happening here. If I look at the logs in our proxy manager for this service, I can see that both of the service calls do return a response very quickly, but the second service call seems to get stuck somewhere in the code and never actually lets go of the connection until the timeout value is reached. I've been trying to track down the cause of this for quite a while, but have been unsuccessful.

I've read some mixed opinions as to whether or not CXF client proxies are thread-safe, but I was under the impression that they were. If this actually not the case, and I should be creating a new client proxy for each invocation, or use a pool of proxies?

cloudwalker
  • 2,346
  • 1
  • 31
  • 69

1 Answers1

0

Turns out that it is an issue with the proxy not being thread-safe. What I wound up doing was leveraging a solution kind of like one posted at the bottom of this post: Is this JAX-WS client call thread safe? - I created a pool for the proxies and I use that to access proxies from multiple threads in a thread-safe manner. This seems to work out pretty well.

public class JaxWSServiceProxyPool<T> extends GenericObjectPool<T> {
    JaxWSServiceProxyPool(Supplier<T> factory, GenericObjectPoolConfig poolConfig) {
        super(new BasePooledObjectFactory<T>() {
            @Override
            public T create() throws Exception {
                return factory.get();
            }

            @Override
            public PooledObject<T> wrap(T t) {
                return new DefaultPooledObject<>(t);
            }
        }, poolConfig != null ? poolConfig : new GenericObjectPoolConfig());
    }
}

I then created a simple "registry" class to keep references to various pools.

@Component
public class JaxWSServiceProxyPoolRegistry {
    private static final Map<Class, JaxWSServiceProxyPool> registry = new HashMap<>();

    public synchronized <T> void register(Class<T> serviceTypeClass, Supplier<T> factory, GenericObjectPoolConfig poolConfig) {
        Assert.notNull(serviceTypeClass);
        Assert.notNull(factory);
        if (!registry.containsKey(serviceTypeClass)) {
            registry.put(serviceTypeClass, new JaxWSServiceProxyPool<>(factory, poolConfig));
        }
    }

    public <T> void register(Class<T> serviceTypeClass, Supplier<T> factory) {
        register(serviceTypeClass, factory, null);
    }

    @SuppressWarnings("unchecked")
    public <T> JaxWSServiceProxyPool<T> getServiceProxyPool(Class<T> serviceTypeClass) {
        Assert.notNull(serviceTypeClass);
        return registry.get(serviceTypeClass);
    }
}

To use it, I did:

JaxWSServiceProxyPoolRegistry jaxWSServiceProxyPoolRegistry = new JaxWSServiceProxyPoolRegistry();
jaxWSServiceProxyPoolRegistry.register(ConsumerSupportService.class,
            this::buildConsumerSupportServiceClient,
            getConsumerSupportServicePoolConfig());

Where buildConsumerSupportServiceClient uses a JaxWsProxyFactoryBean to build up the client.

To retrieve an instance from the pool I inject my registry class and then do:

JaxWSServiceProxyPool<ConsumerSupportService> consumerSupportServiceJaxWSServiceProxyPool = jaxWSServiceProxyPoolRegistry.getServiceProxyPool(ConsumerSupportService.class);

And then borrow/return the object from/to the pool as necessary.

This seems to work well so far. I've executed some fairly heavy load tests against it and it's held up.

cloudwalker
  • 2,346
  • 1
  • 31
  • 69