【踩坑】OpenStack4j使用过程中关于OSClientSession被更改的问题记录

OpenStack4j是一个OpenStack的Java SDK。

问题描述

在同一个代码处理线程中,首先获取了 projectA 的 OSClient 对象 OSClientA,然后又获取了 projectB 的 OSClient 对象 OSClientB。
后续在用 OSClientA 去调用某个 service(比如 BlockVolumeService)去创建资源(比如 volume)的时候,期望创建在 projectA 下面,结果创建的资源却在 projectB 下面。

查找原因

经过跟踪 OpenStack4j 中获取 OSClient 和 调用具体 service 的相关源码后,发现问题,在于 OSClientSession 类中使用 ThreadLocal 变量 sessions 将获取的 OSClient 存下来。
后续在创建资源的时候,使用从 sessions 中取出的OSClient ,调用 OpenStack 的 API 接口。
关键在于,第二次获取 OSClientB 的时候,会将 sessions 中存的 OSClient 更新,将原先的 OSClientA 给替换为 OSClientB。
也就造成了,尽管是用 OSClientA 去创建资源,但是实际使用的 OSClient 已经被改了,也就是是用 OSClientB 的相关参数去创建的。

源码分析

获取 OSClient 的代码在 OSAuthenticator#authenticateV3(默认使用的是v3版本)。

......
    private static OSClientV3 authenticateV3(KeystoneAuth auth, SessionInfo info, Config config) {
        if (auth.getType().equals(Type.TOKENLESS)){
            ......
        }

        # 调用 OpenStack keystone 的认证接口
        HttpRequest<KeystoneToken> request = HttpRequest.builder(KeystoneToken.class)
                .header(ClientConstants.HEADER_OS4J_AUTH, TOKEN_INDICATOR).endpoint(info.endpoint)
                .method(HttpMethod.POST).path("/auth/tokens").config(config).entity(auth).build();

        HttpResponse response = HttpExecutor.create().execute(request);

        if (response.getStatus() >= 400) {
            try {
                throw mapException(response.getStatusMessage(), response.getStatus());
            } finally {
                HttpEntityHandler.closeQuietly(response);
            }
        }
        KeystoneToken token = response.getEntity(KeystoneToken.class);
        token.setId(response.header(ClientConstants.HEADER_X_SUBJECT_TOKEN));

        .......

        String reqId = response.header(ClientConstants.X_OPENSTACK_REQUEST_ID);

        # info.reLinkToExistingSession 在前面的调用过程中传参是 false。
        if (!info.reLinkToExistingSession) {
                # 创建了一个 OSClient,OSClientSessionV3 是 v3 版本的实现类
        	OSClientSessionV3 v3 = OSClientSessionV3.createSession(token, info.perspective, info.provider, config);
        	v3.reqId = reqId;
            return v3;
        }

        OSClientSessionV3 current = (OSClientSessionV3) OSClientSessionV3.getCurrent();
        current.token = token;
       
        current.reqId = reqId;
        return current;
    }

OSClientSessionV3#createSession,也是关键的地方。

        public static OSClientSessionV3 createSession(Token token, Facing perspective, CloudProvider provider, Config config) {
            return new OSClientSessionV3(token, token.getEndpoint(), perspective, provider, config);
        }
......
    public static class OSClientSessionV3 extends OSClientSession<OSClientSessionV3, OSClientV3> implements OSClientV3 {

        Token token;
        
        protected String reqId;

        private OSClientSessionV3(Token token, String endpoint, Facing perspective, CloudProvider provider, Config config) {
            this.token = token;
            this.config = config;
            this.perspective = perspective;
            this.provider = provider;
            # 重点在这里
            sessions.set(this);
        }
......

接着看一下 sessions 这个变量

public abstract class OSClientSession<R, T extends OSClient<T>> implements EndpointTokenProvider {
    
    private static final Logger LOG = LoggerFactory.getLogger(OSClientSession.class);  
    # 可以看到 sessions 是一个  ThreadLocal 变量,而每一次创建新的 OSClientSession,会调用 set 方法,
    # 覆盖之前的 OSClientSession。
    @SuppressWarnings("rawtypes")
    private static final ThreadLocal<OSClientSession> sessions = new ThreadLocal<OSClientSession>();

    Config config;
    Facing perspective;
    String region;
    Set<ServiceType> supports;
    CloudProvider provider;
    Map<String, ? extends Object> headers;
    EndpointURLResolver fallbackEndpointUrlResolver = new DefaultEndpointURLResolver();

由以上的源码知道,每次获取 OSClient(即创建新的OSClientSessionV3)的时候,会在 sessions 中覆盖之前的。

看完获取的过程,再去确认一下调用具体 service 创建资源的时候,是否是从 sessions 中取出的 OSClient。
无论哪个 service 中方法,最终都是使用统一的 http 调用方法 HttpExecutorServiceImpl#invokeRequest

......
    private <R> HttpResponse invokeRequest(HttpCommand<R> command) throws Exception {
        Response response = command.execute();
        if (command.getRetries() == 0 && response.getStatus() == 401 && !command.getRequest().getHeaders().containsKey(ClientConstants.HEADER_OS4J_AUTH))
        {
            # 重点看这个方法的实现,同样是 OSAuthenticator 类中的
            OSAuthenticator.reAuthenticate();
            command.getRequest().getHeaders().put(ClientConstants.HEADER_X_AUTH_TOKEN, OSClientSession.getCurrent().getTokenId());
            return invokeRequest(command.incrementRetriesAndReturn());
        }
        return HttpResponseImpl.wrap(response);
    }

OSAuthenticator.reAuthenticate()

    /**
     * Re-authenticates/renews the token for the current Session
     */
    @SuppressWarnings("rawtypes")
    public static void reAuthenticate() {

        LOG.debug("Re-Authenticating session due to expired Token or invalid response");

        OSClientSession session = OSClientSession.getCurrent();

        switch (session.getAuthVersion()) {
        case V2:
            KeystoneAccess access = ((OSClientSessionV2) session).getAccess().unwrap();
            SessionInfo info = new SessionInfo(access.getEndpoint(), session.getPerspective(), true,
                    session.getProvider());
            Auth auth = (Auth) ((access.isCredentialType()) ? access.getCredentials() : access.getTokenAuth());
            authenticateV2((org.openstack4j.openstack.identity.v2.domain.Auth) auth, info, session.getConfig());
            break;
        case V3:
        default:
            Token token = ((OSClientSessionV3) session).getToken();
            info = new SessionInfo(token.getEndpoint(), session.getPerspective(), true, session.getProvider());
            # 从 sessions 中获取 OSClientSessionV3 之后,同样调用 authenticateV3 认证
            authenticateV3((KeystoneAuth) token.getCredentials(), info, session.getConfig());
            break;
        }
    }

虽然和获取 OSClient 的时候一样,都调用了OSAuthenticator#authenticateV3。但是需要注意,上面说到 info.reLinkToExistingSession 这个参数在获取的时候传参为 false,而这里的传参是 true。
代表它会重新连接已经存在的 Session。

......
    private static OSClientV3 authenticateV3(KeystoneAuth auth, SessionInfo info, Config config) {
        ......

        # info.reLinkToExistingSession 在这里传参是 true,所以不会再创建新的。
        if (!info.reLinkToExistingSession) {
                # 创建了一个 OSClient,OSClientSessionV3 是 v3 版本的实现类
        	OSClientSessionV3 v3 = OSClientSessionV3.createSession(token, info.perspective, info.provider, config);
        	v3.reqId = reqId;
            return v3;
        }
        # 取出当前的 OSClient,直接返回。
        OSClientSessionV3 current = (OSClientSessionV3) OSClientSessionV3.getCurrent();
        current.token = token;
       
        current.reqId = reqId;
        return current;
    }

结论

从上面的源码分析结合我的问题可以得知,在 OSAuthenticator.reAuthenticate() 取出的当前的 OSClient 是 OSClientB,而不是 OSClientA,所以导致了资源创建在了 projectB 下面。

分享一下自己踩坑的问题分析,希望大家都可以及时发现并避免因此出现意想不到的 Bug。

posted @ 2020-05-02 18:27  zhaoyixin96  阅读(686)  评论(0编辑  收藏  举报