zoukankan      html  css  js  c++  java
  • 【踩坑】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。

  • 相关阅读:
    数据库连接池
    Apache- DBUtils框架学习
    权限表的设计
    Java的I/O对文件的操作
    Java下载文件
    Java连接MySQL数据库
    C#用log4net记录日志
    C#多线程和线程池
    C#利用反射动态调用DLL并返回结果,和获取程序集的信息
    CephRGW 在多个RGW负载均衡场景下,RGW 大文件并发分片上传功能验证
  • 原文地址:https://www.cnblogs.com/zhaoyixin96/p/12719983.html
Copyright © 2011-2022 走看看