hdfs源码解析之DFSClient

1、DFSClient类简介

    DFSClient 是 Hadoop 分布式文件系统(HDFS)中的一个核心类,用于客户端与 HDFS 之间的交互。它提供了一组方法,使客户端应用程序可以方便地与 HDFS 进行通信,包括文件的读取、写入、创建、删除、重命名等操作。DFSClient 封装了与 NameNode 和 DataNode 的通信细节,使得客户端开发者可以通过高级 API 进行文件系统操作,而不必关心底层的实现细节。

2、DFSClient主要功能

2.1、文件读取和写入

  • 提供方法用于读取和写入 HDFS 上的文件。
  • 例如,open 方法用于打开文件以读取,create 方法用于创建新文件以写入。

2.2、文件操作

  • 支持文件的创建、删除、重命名、追加等操作。
  • 例如,delete 方法用于删除文件或目录,rename 方法用于重命名文件或目录。

2.3、目录操作

  • 支持创建、删除和列出目录。
  • 例如,mkdirs 方法用于创建目录,listPaths 方法用于列出目录内容。

2.4、获取文件和目录信息

  • 提供方法获取文件和目录的元数据信息。
  • 例如,getFileInfo 方法用于获取文件或目录的详细信息,getLocatedBlocks 方法用于获取文件的块位置。

2.5、与NN、DN通信

  • 管理与 NameNode 的通信,用于获取文件的元数据和块位置信息。
  • 管理与 DataNode 的通信,用于读取和写入实际的数据块。

3、DFSClient核心源码

    DFSClient源码主要包括:创建客户端连接(配置获取、令牌处理、连接地址解析)

3.1、构造方法

3.1.1、代码概述

该构造函数已废弃,接受一个Configuration对象,并调用另一个构造函数获取NameNode地址

  @Deprecated
  public DFSClient(Configuration conf) throws IOException {
    this(DFSUtilClient.getNNAddress(conf), conf);
  }

该构造函数接受一个InetSocketAddress对象和一个Configuration对象,并将InetSocketAddress 转换为URI然后调用另一个基于URI的构造函数

  public DFSClient(InetSocketAddress address, Configuration conf)
      throws IOException {
    this(DFSUtilClient.getNNUri(address), conf);
  }

该构造函数接受一个URI对象和一个Configuration对象,并将FileSystem.Statistics参数设置为 null,然后调用另一个更完整的构造函数

  public DFSClient(URI nameNodeUri, Configuration conf) throws IOException {
    this(nameNodeUri, conf, null);
  }

该构造函数接受一个URI对象、一个Configuration对象和一个FileSystem.Statistics对象,然后调用最完整的构造函数

  public DFSClient(URI nameNodeUri, Configuration conf,
      FileSystem.Statistics stats) throws IOException {
    this(nameNodeUri, null, conf, stats);
  }

 最底层构造函数,该方法不建议直接调用。

  @VisibleForTesting
  public DFSClient(URI nameNodeUri, ClientProtocol rpcNamenode,
      Configuration conf, FileSystem.Statistics stats) throws IOException {
    // Copy only the required DFSClient configuration
    this.tracer = FsTracer.get(conf);
    this.dfsClientConf = new DfsClientConf(conf);
    this.conf = conf;
    this.stats = stats;
    this.socketFactory = NetUtils.getSocketFactory(conf, ClientProtocol.class);
    this.dtpReplaceDatanodeOnFailure = ReplaceDatanodeOnFailure.get(conf);
    this.dtpReplaceDatanodeOnFailureReplication = (short) conf
        .getInt(HdfsClientConfigKeys.BlockWrite.ReplaceDatanodeOnFailure.
                MIN_REPLICATION,
            HdfsClientConfigKeys.BlockWrite.ReplaceDatanodeOnFailure.
                MIN_REPLICATION_DEFAULT);
    LOG.debug("Sets {} to {}",
        HdfsClientConfigKeys.BlockWrite.ReplaceDatanodeOnFailure.
            MIN_REPLICATION, dtpReplaceDatanodeOnFailureReplication);
    this.ugi = UserGroupInformation.getCurrentUser();

    this.namenodeUri = nameNodeUri;
    this.clientName = "DFSClient_" + dfsClientConf.getTaskId() + "_" +
        ThreadLocalRandom.current().nextInt()  + "_" +
        Thread.currentThread().getId();
    int numResponseToDrop = conf.getInt(
        DFS_CLIENT_TEST_DROP_NAMENODE_RESPONSE_NUM_KEY,
        DFS_CLIENT_TEST_DROP_NAMENODE_RESPONSE_NUM_DEFAULT);
    ProxyAndInfo<ClientProtocol> proxyInfo = null;
    AtomicBoolean nnFallbackToSimpleAuth = new AtomicBoolean(false);

    if (numResponseToDrop > 0) {
      // This case is used for testing.
      LOG.warn("{} is set to {} , this hacked client will proactively drop responses",
          DFS_CLIENT_TEST_DROP_NAMENODE_RESPONSE_NUM_KEY, numResponseToDrop);
      proxyInfo = NameNodeProxiesClient.createProxyWithLossyRetryHandler(conf,
          nameNodeUri, ClientProtocol.class, numResponseToDrop,
          nnFallbackToSimpleAuth);
    }

    if (proxyInfo != null) {
      this.dtService = proxyInfo.getDelegationTokenService();
      this.namenode = proxyInfo.getProxy();
    } else if (rpcNamenode != null) {
      // This case is used for testing.
      Preconditions.checkArgument(nameNodeUri == null);
      this.namenode = rpcNamenode;
      dtService = null;
    } else {
      Preconditions.checkArgument(nameNodeUri != null,
          "null URI");
      proxyInfo = NameNodeProxiesClient.createProxyWithClientProtocol(conf,
          nameNodeUri, nnFallbackToSimpleAuth);
      this.dtService = proxyInfo.getDelegationTokenService();
      this.namenode = proxyInfo.getProxy();
    }

    String localInterfaces[] =
        conf.getTrimmedStrings(DFS_CLIENT_LOCAL_INTERFACES);
    localInterfaceAddrs = getLocalInterfaceAddrs(localInterfaces);
    if (LOG.isDebugEnabled() && 0 != localInterfaces.length) {
      LOG.debug("Using local interfaces [{}] with addresses [{}]",
          Joiner.on(',').join(localInterfaces),
          Joiner.on(',').join(localInterfaceAddrs));
    }

    Boolean readDropBehind =
        (conf.get(DFS_CLIENT_CACHE_DROP_BEHIND_READS) == null) ?
            null : conf.getBoolean(DFS_CLIENT_CACHE_DROP_BEHIND_READS, false);
    Long readahead = (conf.get(DFS_CLIENT_CACHE_READAHEAD) == null) ?
        null : conf.getLongBytes(DFS_CLIENT_CACHE_READAHEAD, 0);
    this.serverDefaultsValidityPeriod = conf.getTimeDuration(
        DFS_CLIENT_SERVER_DEFAULTS_VALIDITY_PERIOD_MS_KEY,
        DFS_CLIENT_SERVER_DEFAULTS_VALIDITY_PERIOD_MS_DEFAULT,
        TimeUnit.MILLISECONDS);
    Boolean writeDropBehind =
        (conf.get(DFS_CLIENT_CACHE_DROP_BEHIND_WRITES) == null) ?
            null : conf.getBoolean(DFS_CLIENT_CACHE_DROP_BEHIND_WRITES, false);
    this.defaultReadCachingStrategy =
        new CachingStrategy(readDropBehind, readahead);
    this.defaultWriteCachingStrategy =
        new CachingStrategy(writeDropBehind, readahead);
    this.clientContext = ClientContext.get(
        conf.get(DFS_CLIENT_CONTEXT, DFS_CLIENT_CONTEXT_DEFAULT),
        dfsClientConf, conf);

    if (dfsClientConf.getHedgedReadThreadpoolSize() > 0) {
      this.initThreadsNumForHedgedReads(dfsClientConf.
          getHedgedReadThreadpoolSize());
    }

    this.initThreadsNumForStripedReads(dfsClientConf.
        getStripedReadThreadpoolSize());
    this.saslClient = new SaslDataTransferClient(
        conf, DataTransferSaslUtil.getSaslPropertiesResolver(conf),
        TrustedChannelResolver.getInstance(conf), nnFallbackToSimpleAuth);
  }

3.1.2、重点剖析

DFSClient的核心构建方式是传入namenode节点对应的URI以及配置信息,也是我们构建DFSClient通常使用的方法

public DFSClient(URI nameNodeUri, Configuration conf) throws IOException {
  this(nameNodeUri, conf, null);
}

3.2、委托令牌处理      

        这段源码是一个用于续约和取消 HDFS 委托令牌(Delegation Token)的 Renewer 类,它继承自 TokenRenewer 类。主要功能是通过与 NameNode 通信,维护和管理委托令牌的生命周期。

3.2.1、代码概述

3.2.2、重点剖析

  • static静态代码块为初始化hdfs配置文件;
  • handleKind方法用于判断是否处理指定类型的委托令牌,在当前源码中会默认判定是否为HDFS的委托令牌类型;
  • renew 方法用于续约委托令牌。它通过 getNNProxy 方法获取到与委托令牌对应的 NameNode 代理,然后调用 renewDelegationToken 方法进行委托令牌的续约操作;
  • cancel 方法用于取消委托令牌。它也通过 getNNProxy 方法获取 NameNode 代理,然后调用 cancelDelegationToken 方法执行委托令牌的取消操作;
  • getNNProxy 方法根据委托令牌获取对应的 NameNode 代理。它首先根据委托令牌的信息构建 URI,然后通过 NameNodeProxiesClient 类的静态方法创建 NameNode 的代理对象,并返回该代理对象。

3.3、getLocalInterfaceAddrs

3.3.1、代码概述

       这个方法的作用是接受一个接口名称的数组,并根据每个接口名称解析成对应的本地地址(可以是 IP 地址、子网或域名)。它首先尝试将接口名称视为一个 IP 地址,如果不是,则检查它是否是一个有效的子网,如果仍然不是,则假定它是一个域名,并通过 DNS 解析。最终,所有解析出的地址都被封装为 InetSocketAddress 对象,并返回一个包含这些地址的数组。

private static SocketAddress[] getLocalInterfaceAddrs(
      String interfaceNames[]) throws UnknownHostException {
    List<SocketAddress> localAddrs = new ArrayList<>();
    for (String interfaceName : interfaceNames) {
      if (InetAddresses.isInetAddress(interfaceName)) {
        localAddrs.add(new InetSocketAddress(interfaceName, 0));
      } else if (NetUtils.isValidSubnet(interfaceName)) {
        for (InetAddress addr : NetUtils.getIPs(interfaceName, false)) {
          localAddrs.add(new InetSocketAddress(addr, 0));
        }
      } else {
        for (String ip : DNS.getIPs(interfaceName, false)) {
          localAddrs.add(new InetSocketAddress(ip, 0));
        }
      }
    }
    return localAddrs.toArray(new SocketAddress[localAddrs.size()]);
  }

3.3.2、重点剖析

  1. 该方法首先检查interfaceName是否是一个有效的IP地址:
  2. 如果不是IP地址,检查interfaceName是否是一个有效的子网:
  3. 如果是有效的子网,获取该子网中的所有IP地址,并将每个IP地址封装为InetSocketAddress对象,添加到localAddrs列表中。
  4. 如果既不是IP地址也不是子网,假定它是一个域名:
  5. 通过DNS解析获取该域名的所有IP地址,并将每个IP地址封装为InetSocketAddress对象,添加到localAddrs列表中。

3.4、getRandomLocalInterfaceAddr

3.4.1、代码概述

        这个方法的作用是从一组预先配置的本地接口地址 (localInterfaceAddrs 数组) 中随机选择一个地址并返回。

SocketAddress getRandomLocalInterfaceAddr() {
    if (localInterfaceAddrs.length == 0) {
      return null;
    }
    final int idx = r.nextInt(localInterfaceAddrs.length);
    final SocketAddress addr = localInterfaceAddrs[idx];
    LOG.debug("Using local interface {}", addr);
    return addr;
  }

3.4.2、重点剖析

  1. 检查 localInterfaceAddrs 数组是否为空,如果为空则返回 null
  2. 使用随机数生成器 r 生成一个随机索引 idx
  3. 获取并返回 localInterfaceAddrs 数组中对应索引 idxSocketAddress 对象。
  4. 在返回之前,记录调试日志以便于跟踪选中的本地接口地址。

3.5、读写超时时间判定

3.5.1、代码概述

        这段代码包含两个方法:getDatanodeWriteTimeoutgetDatanodeReadTimeout,它们用于计算数据节点写入和读取的超时时间。每个方法都接收一个参数 numNodes,表示数据节点的数量。

int getDatanodeWriteTimeout(int numNodes) {
    final int t = dfsClientConf.getDatanodeSocketWriteTimeout();
    return t > 0? t + HdfsConstants.WRITE_TIMEOUT_EXTENSION*numNodes: 0;
}

int getDatanodeReadTimeout(int numNodes) {
    final int t = dfsClientConf.getSocketTimeout();
    return t > 0? HdfsConstants.READ_TIMEOUT_EXTENSION*numNodes + t: 0;
}

3.5.2、重点剖析

  1. 通过dfsclientconf获取写入\读取超时时间t;
  2. 如果t大于0则返回 t 加上一个扩展超时时间,这个扩展超时时间是常量 HdfsConstants.WRITE_TIMEOUT_EXTENSION 乘以 numNodes(数据节点数量)
  3. 如果t<=0,则返回0

3.6、租约管理

3.6.1、代码概述

        这段代码定义了三个方法:getLeaseRenewerbeginFileLeaseendFileLease,用于管理HDFS中的文件租约。文件租约机制确保文件在写入过程中不会被其他客户端修改或删除。

public LeaseRenewer getLeaseRenewer() {
    return LeaseRenewer.getInstance(
        namenodeUri != null ? namenodeUri.getAuthority() : "null", ugi, this);
  }

  /** Get a lease and start automatic renewal */
  private void beginFileLease(final String key, final DFSOutputStream out) {
    synchronized (filesBeingWritten) {
      putFileBeingWritten(key, out);
      LeaseRenewer renewer = getLeaseRenewer();
      boolean result = renewer.put(this);
      if (!result) {
        // Existing LeaseRenewer cannot add another Daemon, so remove existing
        // and add new one.
        LeaseRenewer.remove(renewer);
        renewer = getLeaseRenewer();
        renewer.put(this);
      }
    }
  }

  /** Stop renewal of lease for the file. */
  void endFileLease(final String renewLeaseKey) {
    synchronized (filesBeingWritten) {
      removeFileBeingWritten(renewLeaseKey);
      // remove client from renewer if no files are open
      if (filesBeingWritten.isEmpty()) {
        getLeaseRenewer().closeClient(this);
      }
    }
  }

3.6.2、重点剖析

  • 获取租约续约器getLeaseRenewer 方法返回一个 LeaseRenewer 实例,用于管理租约的续约。        
    • 获取租约续约器
    • 调用 LeaseRenewer.getInstance 方法获取 LeaseRenewer 实例。
    • 如果 namenodeUri 不为空,则使用其权限部分(authority),否则使用 "null"。ugi(用户组信息)和当前 DFSClient 实例(this)作为参数传递给 LeaseRenewer.getInstance
  • 开始文件租约beginFileLease 方法将文件添加到写入记录中,并确保当前客户端的租约续约器能够处理该文件的续约。
    • 使用 key 和 out(DFSOutputStream 实例)调用 putFileBeingWritten 方法,记录正在写入的文件;
    • 获取 LeaseRenewer 实例;
    • 调用 renewer.put(this) 方法将当前客户端添加到租约续约器中;
    • 如果返回结果为 false(表示现有的 LeaseRenewer 不能添加新的守护线程),则移除现有的 LeaseRenewer,获取新的 LeaseRenewer 实例,并将当前客户端添加到新的 LeaseRenewer 中;
  • 结束文件租约endFileLease 方法移除文件写入记录,并在没有文件写入时关闭客户端的租约续约
    • 使用 renewLeaseKey 调用 removeFileBeingWritten 方法,从记录中移除正在写入的文件
    • 如果没有文件在写入(filesBeingWritten 为空),则获取 LeaseRenewer 实例,调用 renewer.closeClient(this) 方法,关闭当前客户端的租约续约。

todo,未完待续

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/722626.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

探索磁力搜索引擎:互联网资源获取的新视角

在当今数字化社会中&#xff0c;寻找和获取网络资源变得更加便捷和多样化。磁力搜索引擎作为这一趋势的一部分&#xff0c;提供了一种新颖而有效的方法来定位和获取用户所需的文件、媒体和其他数字内容。本文将深入探讨磁力搜索引擎的工作原理、使用场景及其在网络文化中的影响…

BizDevOps全局建设思路:横向串联,纵向深化

本文来自腾讯蓝鲸智云社区用户&#xff1a;CanWay BizDevOps概述 IT技术交付实践方法在不断迭代中持续优化。在工业化时代&#xff0c;Biz&#xff08;业务&#xff09;、Dev&#xff08;开发&#xff09;、Ops&#xff08;运维&#xff09;三者往往相对分离&#xff0c;甚至有…

JAVA的优势是什么?

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「java的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“666”之后私信回复“666”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; java编程语言自1995年问世…

家人们,我最近迷上了食家巷的方形饼

那独特的方形造型&#xff0c;超级可爱。&#x1f44f;刚出炉的方形饼&#xff0c;热气腾腾&#xff0c;散发着诱人的香气。&#x1f60b;咬一口&#xff0c;酥脆的外皮“咔滋”作响&#xff0c;里面的面饼却又十分绵软&#xff0c;口感层次超丰富&#xff01;&#x1f929;无论…

【查看显卡信息】——Ubuntu和windows

1、VMware虚拟机 VMware虚拟机上不能使用CUDA/CUDNN&#xff0c;也安装不了显卡驱动 查看显卡信息&#xff1a; lspci | grep -i vga 不会显示显卡信息&#xff0c;只会输出VMware SVGA II Adapter&#xff0c;表示这是一个虚拟机&#xff0c;无法安装和使用显卡驱动 使用上…

Chromium 开发指南2024 Mac篇-开始编译Chromium(五)

1.引言 在之前的指南中&#xff0c;我们已经详细介绍了在 macOS 上编译和开发 Chromium 的准备工作。您学会了如何安装和配置 Xcode&#xff0c;如何下载和配置 depot_tools&#xff0c;以及如何获取 Chromium 的源代码。通过这些步骤&#xff0c;您的开发环境已经搭建完毕&am…

压力应变桥信号变送光电隔离放大模块PCB焊接式 差分信号输入0-10mV/0-20mV/0-±10mV/0-±20mV转0-5V/0-10V/4-20mA

概述&#xff1a; IPO压力应变桥信号处理系列隔离放大器是一种将差分输入信号隔离放大、转换成按比例输出的直流信号混合集成厚模电路。产品广泛应用在电力、远程监控、仪器仪表、医疗设备、工业自控等行业。该模块内部嵌入了一个高效微功率的电源&#xff0c;向输入端和输出端…

必看!!! 2024 最新 PG 硬核干货大盘点(上)

PGConf.dev&#xff08;原名PGCon&#xff0c;从2007年至2023年&#xff09;首次在风景如画的加拿大温哥华市举办。此次重新定位的会议带来了全新的视角和多项新的内容&#xff0c;参会体验再次升级。尽管 PGCon 历来更侧重于开发者&#xff0c;吸引来自世界各地的资深开发者、…

1950 Springboot汽修技能点评系统idea开发mysql数据库APP应用java编程计算机网页源码maven项目

一、源码特点 springboot 汽修技能点评系统是一套完善的信息系统&#xff0c;结合springboot框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用springboot框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统 具有完整的源代码和数据库&…

JavaScript-拓展简单和引用数据类型

学习目标&#xff1a; 掌握拓展简单和引用数据类型 学习内容&#xff1a; 拓展-术语解释拓展-基本数据类型和引用数据类型 拓展-术语解释&#xff1a; 拓展-基本数据类型和引用数据类型&#xff1a; 简单类型又叫做基本数据类型或者值类型&#xff0c;复杂类型又叫做引用类型…

LLM 中什么是Prompts?如何使用LangChain 快速实现Prompts 一

LLM 中什么是Prompts&#xff1f;如何使用LangChain 快速实现Prompts 一 Prompt是一种基于自然语言处理的交互方式&#xff0c;它通过机器对自然语言的解析&#xff0c;实现用户与机器之间的沟通。 Prompt主要实现方式是通过建立相应的语料库和语义解析模型&#xff0c;来将自…

STM32学习笔记(九)--串口 UART/USART详解

&#xff08;1&#xff09;配置步骤1.开启RCC外设时钟 开启GPIO以及USART外设2.初始化GPIO 配置TX复用输出 RX输入3.配置USART初始化结构体4.配置串口中断 ITConfig以及NVIC&#xff08;如果需要USART中断&#xff09;5.开启USART &#xff08;2&#xff09;代码示例 案例1 串…

【2024】kafka streams的详细使用与案例练习(2)

目录 前言使用1、整体结构1.1、序列化 2、 Kafka Streams 常用的 API2.1、 StreamsBuilder2.2、 KStream 和 KTable2.3、 filter和 filterNot2.4、 map 和 mapValues2.5、 flatMap 和 flatMapValues2.6、 groupByKey 和 groupBy2.7、 count、reduce 和 aggregate2.8、 join 和 …

深圳比创达|EMI电磁干扰行业:从挑战到机遇的蜕变

在当今科技日新月异的时代&#xff0c;电磁干扰&#xff08;EMI&#xff09;已成为影响电子设备性能和稳定性的重要因素。EMI电磁干扰行业因此应运而生&#xff0c;致力于研究和解决电磁干扰问题&#xff0c;确保电子设备的正常运行。 一、EMI电磁干扰行业面临的挑战 随着电子…

Java学习 (一) 环境安装及入门程序

一、安装java环境 1、获取软件包 https://www.oracle.com/java/technologies/downloads/ .exe 文件一路装过去就行&#xff0c;最好别装c盘 &#xff0c;我这里演示的时候是云主机只有C盘 2、配置环境变量 我的电脑--右键属性--高级系统设置--环境变量 在环境变量中添加如下配…

Pycharm的基础使用

Pycharm的基础使用 一、修改主题 第一步&#xff1a;点击file->settings 第二步&#xff1a;找到Appearance&Behavior->Appearance->Theme选择主题 有五种主题可以选 二、修改默认字体和大小 第一步&#xff1a;打开设置与上面修改主题第一步一样&#xff1b…

可以把 FolkMQ 内嵌到 SpringBoot3 项目里(可内嵌的消息中间件)

之前发了《把 FolkMQ 内嵌到 SpringBoot2 项目里&#xff08;比如 “诺依” 啊&#xff09;》。有人说都淘态了&#xff0c;有什么好内嵌的。。。所以再发个 SpringBoot3 FolkMQ 是一个 “纯血国产” 的消息中间件。支持内嵌、单机、集群、多重集群等多种部署方式。 内嵌版&am…

【学习】程序员资源网站

1 书栈网 简介&#xff1a;书栈网是程序员互联网IT开源编程书籍、资源免费阅读的网站&#xff0c;在书栈网你可以找到很多书籍、笔记资源。在这里&#xff0c;你可以根据热门收藏和阅读查看大家都在看什么&#xff0c;也可以根据技术栈分类找到对应模块的编程资源&#xff0c;…

elasticsearch hanlp插件远程词典配置

elasticsearch hanlp插件远程词典配置 背景远程词典配置新增远程词典文件修改hanlp-remote.xml自动加载词典 远程词典测试 背景 在使用elasticsearch的过程中&#xff0c;总会遇到与分词相关的需求&#xff0c;这里将针对常用的elasticsearch hanlp&#xff08;后面统称为 es …

uniapp app一键登录

一键登录不需要单独写页面&#xff0c;uniapp 有原生的页面 第一步&#xff0c;登录Dcloud后台》我的应用》点击应用名称 填写完点击 uniCloud模块新建一个服务空间》选择免费 , 创建完点击一键登录&#xff0c;添加应用&#xff0c;这个需要审核&#xff0c;“大概一天左右”…