Apache Solr のバグの検索クエリ

PHPz
リリース: 2024-08-01 03:21:13
オリジナル
353 人が閲覧しました

もう一度、Apache 製品を確認します。今回は、オープンソースの検索サーバー プラットフォームである Solr を選択しました。 Solr を使用すると、データベースやオンライン リソース内の情報を迅速かつ効率的に検索できます。このような複雑なタスクに直面すると、経験豊富な Apache 開発者であっても間違いを犯しやすくなります。この記事では、このようなタイプの間違いについて見ていきます。

Search query for bugs in Apache Solr

あなたは誰ですか、ソル?

少し前に、最も有名な Apache プロジェクトの 1 つである NetBeans IDE を確認しました。チェック中に、アナライザーによって発行された多くの興味深い警告が見つかりました。ちなみに、開発者はすぐにそれらに気づき、私が行う前にプル リクエストを作成しました :) 今回は、彼らのもう 1 つの大きな製品である Solr 全文検索プラットフォームを検討することにしました。

2006 年に初めて導入された Apache Solr は、動的クラスタリングやデータベース統合から複雑な形式のドキュメントの処理まで、幅広い機能を提供します。この検索エンジンを使用すると、Web サイト上の情報を高速に検索および分析できます。また、Linux 上で実行されるハードウェア上で検索サーバーをホストする機能も提供します。 Solrには...

簡単に言うと、あまり細かいことで煩わしさを感じたくないのです。ビッグデータを最適化する便利なソフトウェア プラットフォームであることを知っておくだけで十分です。そのソース コードを見て、興味深いものや珍しいものを探してみてはいかがでしょうか。それがまさに今私たちがやろうとしていることです。

彼らはここで何かを混ぜ合わせた

プログラマは、演算子の混同によるタイプミスから決して安全ではありません。ここにその 1 つがあります:

リーリー

このメソッドは、meta 変数を使用してシステム スナップショットに関する情報を保存します。上記のコード部分には、 meta != null のチェックもあります。その場合、IllegalArgumentExceptionがスローされます。それだけでも奇妙に思えます。さて、ループ本体を見てみましょう。ここで getReplicaSnapshotsForShard が呼び出されます。ここの meta が常に null であるとすると、NullPointerException が発生します。開発者がタイプミスをして演算子を混同しただけのようです。したがって、metanullと等しい場合は例外がスローされる必要があります。

PVS-Studio アナライザーは校正者として機能し、検出されたタイプミスを報告しました:

V6008 'meta' の null 逆参照。 SolrSnapshotsTool.java 262

演算子を混同した場合のこのようなエラーは、皆さんが思っているよりも一般的であることを付け加えておきたいと思います。たとえば、NetBeans プロジェクトですでに少なくとも 2 つの類似したものを確認しました。


リーリー

V6008 'tm' の null 逆参照。ソースモデル.java 713


リーリー

V6008 「リスナー」の null 逆参照。 MavenArtifactsImplementation.java 613

新しいタイプミスのパターンが見つかった可能性があります。これからも見守っていきます。

次のフラグメントに進みましょう。このクラスでは、開発者が

get メソッドの 1 つで返す内容を混同しました。
リーリー

このクラスは、書かれた特定の数学関数を解析するために設計されています。パーサーの動作を変更するプロパティは 2 つあります。

parseMultipleSources は数値のすべてのソースを分析し、parseToEnd は文字列を含む関数を最後まで解析する必要があるかどうかをチェックします。

ここで、これらのフィールドの

get メソッドと set メソッドを見てみましょう。 parseMultipleSources フィールドは getParseToEnd で返されます。プログラマは、ここでどのフィールドを返すべきかを混同しました。

アナライザーは、返されたフィールドの不一致を簡単に検出します:

V6091 不審なゲッター実装。おそらく代わりに「parseToEnd」フィールドが返されるはずです。 FunctionQParser.java 87、FunctionQParser.java 57

次のコード部分のタイプミスにより、

NullPointerException.が発生する可能性があります。
リーリー

詳しく見てみましょう: まず、

valuenull と比較され、次に次の行で length() メソッドが value に対して呼び出されます。ただし、変数は null になる可能性があります。おそらく、開発者は length() を呼び出す代わりに len 変数を使用すべきでした。 このタイプミスは、アナライザー メッセージのおかげで見つかりました:

V6008 'value' の潜在的な null 逆参照。 IndexSizeEstimator.java 735、IndexSizeEstimator.java 736

タイプミスのある別のコード断片を見てみましょう:

リーリー
ここの

for

ループに注目する価値があります。いつものように、プログラマはループと idx カウンター変数を宣言し、リスト内の項目の数を取得します。ただし、問題があります。ループの各反復では、0: list.get(0) Index. の要素のみが取られます。 PVS-Studio アナライザーが次のエラーを検出しました:

V6016 ループ内の定数インデックスによる「list」オブジェクトの要素への不審なアクセス。 AscEvaluator.java 56

The following example shows two methods with different names. However, they do the same thing.

private static List<Feature> makeFeatures(int[] featureIds) {
  final List<Feature> features = new ArrayList<>();
  for (final int i : featureIds) {
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("value", i);
    final Feature f = Feature.getInstance(solrResourceLoader, 
                       ValueFeature.class.getName(), "f" + i, params);
    f.setIndex(i);
    features.add(f);
  }
  return features;
}

private static List<Feature> makeFilterFeatures(int[] featureIds) {
  final List<Feature> features = new ArrayList<>();
  for (final int i : featureIds) {
    Map<String, Object> params = new HashMap<String, Object>();
    params.put("value", i);
    final Feature f = Feature.getInstance(solrResourceLoader, 
                       ValueFeature.class.getName(), "f" + i, params);
    f.setIndex(i);
    features.add(f);
  }
  return features;
}
ログイン後にコピー

The first one creates a list of the Feature class objects. The second one, based on the name, should return a different type or filter these Features. If the FilterFeature type existed in the source code, we could assume that the developers simply made a typo. However, there's no such type. Maybe the method was copied and the developers forgot about it after copying it.

Anyway, this snippet looks very suspicious. And the analyzer proves this:

V6032 It is odd that the body of method 'makeFeatures' is fully equivalent to the body of another method 'makeFilterFeatures'. TestLTRScoringQuery.java 66, TestLTRScoringQuery.java 79

Forgot to check? Got error on track

If your gut tells you that null checks are unnecessary, don't trust it. In the code below, the "extra" check could've prevented NullPointerException.

public static Map<String, Object> postProcessCollectionJSON(
                                            Map<String, Object> collection) {
  final Map<String, Map<String, Object>> shards = collection != null   // <=
         ? (Map<String, Map<String, Object>>)
           collection.getOrDefault("shards", Collections.emptyMap())
         : Collections.emptyMap();
  final List<Health> healthStates = new ArrayList<>(shards.size());
  shards.forEach(
  ....
  );
  collection.put("health", Health.combine(healthStates).toString());   // <=
  return collection;
}
ログイン後にコピー

In the beginning of the method, the programmer checks if the collection reference is empty. If that's the case, then shards are derived from the collection. The most interesting thing is that healthStates is added to the collection at the end, regardless of whether the collection reference is empty or not.

Here's the analyzer warning for this code fragment:

V6008 Potential null dereference of 'collection'. ClusterStatus.java 303, ClusterStatus.java 335

And in the next example, the developers made an obvious mistake in the class constructor to support parallel distribution of thread work.

public class ParallelStream extends CloudSolrStream 
                            implements Expressible {
  ....
  private transient StreamFactory streamFactory;

  public ParallelStream(String zkHost, 
                        String collection, 
                        String expressionString, 
                        int workers, 
                        StreamComparator comp
) throws IOException {
    TupleStream tStream = this.streamFactory
                              .constructStream(expressionString);  // <=
    init(zkHost, collection, tStream, workers, comp);
  }  
  ....
}
ログイン後にコピー

The error lies in the first line of the constructor body. The streamFactory field is accessed here, but the field isn't initialized. The developers may have forgotten to add some logic in the constructor, or accidently may have written this line.

The PVS-Studio warning:

V6090 Field 'streamFactory' is being used before it was initialized. ParallelStream.java 61

However, they didn't forget to add a check in this method. Although, I think they put it in the wrong place.

private void createNewCollection(final String collection)
 throws InterruptedException {
  ....
  pending.add(completionService.submit(call));
  while (pending != null && pending.size() > 0) {
    Future<Object> future = completionService.take();
    if (future == null) return;
    pending.remove(future);
  }
}
ログイン後にコピー

Let's look at the interaction with the pending field: first the programmer called add, then they decided to make a loop in which they gradually removed elements from the method. The most interesting thing is that they checked that pending isn't null in the loop condition. It looks very suspicious, considering that there's no variable zeroing in the loop body. Seems like they should've added a check before calling the add method as well.

The analyzer warning:

V6060 The 'pending' reference was utilized before it was verified against null. AbstractBasicDistributedZkTestBase.java 1664, AbstractBasicDistributedZkTestBase.java 1665

Lost exception

Like many modern languages, Java has exception handling feature. The most important thing is to not lose them, as it happened here.

private void doSplitShardWithRule(SolrIndexSplitter.SplitMethod splitMethod) 
 throws Exception {
  ....
  try {
    ZkStateReader.from(cloudClient)
                 .waitForState(collectionName, 30, 
                             TimeUnit.SECONDS,
                             SolrCloudTestCase.activeClusterShape(1, 2));
  } catch (TimeoutException e) {
    new RuntimeException("Timeout waiting for
                          1shards and 2 replicas.", e);     // <=
  }
  ....
}
ログイン後にコピー

The error lies in the catch block: the developers created the RuntimeException object there, even added a link to the current intercepted TimeoutException and a message. But they forgot to write the throw keyword. So, the exception is never thrown.

The Lost and Found Bureau, in the form of our analyzer, found the lost exception and notified us about it:

V6006 The object was created but it is not being used. The 'throw' keyword could be missing. ShardSplitTest.java 773

How arithmetic errors interfere with testing

Does testing make software less buggy? Well, humans are the ones who write tests, and they can't help but make mistakes. This is what happened in the following example.

Public class SpellCheckCollatorTest extends SolrTestCaseJ4 {
  private static final int NUM_DOCS_WITH_TERM_EVERYOTHER = 8;
  private static final int NUM_DOCS = 17;
  ....
  @Test
  public void testEstimatedHitCounts() {
    ....
    for (int val = 5; val <= 20; val++) {
      String hitsXPath = xpathPrefix + "long[@name='hits']"; 

      if (val <= NUM_DOCS_WITH_TERM_EVERYOTHER) {
        int max = NUM_DOCS;
        int min = (/* min collected */ val) / 
                  (/* max docs possibly scanned */ NUM_DOCS);
        hitsXPath += "[" + min + " <= . and . <= " + max + "]";
      } 
    ....
    }
  }
  ....
}
ログイン後にコピー

A string containing the min variable, which in turn is the result of dividing val by NUM_DOCS, is written to hitsXPath here. Looking closer, you can see that the maximum and minimum values of val in this fragment are 8 and 5. The NUM_DOCS value is always 17. In all cases, min is zero in integer division. Most likely, the programmer forgot to convert division arguments to real numbers and change the type of the min variable.

We found this error using a brand-new diagnostic rule in the PVS-Studio analyzer:

V6113 The '(val) / (NUM_DOCS)' expression evaluates to 0 because the absolute value of the left operand 'val' is less than the value of the right operand 'NUM_DOCS'. SpellCheckCollatorTest.java 683

Danger of checking objects by reference

The class bellow describes the equalsTo comparison method.

private static class RandomQuery extends Query {
  private final long seed;
  private float density;
  private final List<BytesRef> docValues;
  ....
  private boolean equalsTo(RandomQuery other) {
    return seed == other.seed && 
           docValues == other.docValues && 
           density == other.density;
  }
}
ログイン後にコピー

Comparisons of the seed and density fields almost don't cause any questions (except, perhaps, for the density field that is a real number), because the values directly written into them are considered. However, since this field has a reference type, the docValues comparison via '==' looks very dubious. This check considers only the address and not the internal state of the object.

With such defect you can miss the case when two different lists store the same values because the lists are the same, but the references are different. It seems that when the developers named the equalsTo method, they hardly meant that it should compare references rather than the internal state of objects.

The PVS-Studio analyzer warning:

V6013 Objects 'docValues' and 'other.docValues' are compared by reference. Possibly an equality comparison was intended. TestFieldCacheSortRandom.java 341

Suspicious lack of synchronization

You won't find an error in the next fragment, but it's still potentially there. How's that possible? Take a look at this code and find out why.

public abstract class CachingDirectoryFactory extends DirectoryFactory {
  ....
  private static final Logger log = LoggerFactory.getLogger(....);
  protected Map<String, CacheValue> byPathCache = new HashMap<>();
  protected IdentityHashMap<Directory, CacheValue> byDirectoryCache = 
                                                  new IdentityHashMap<>();
  ....

  private void removeFromCache(CacheValue v) {
    log.debug("Removing from cache: {}", v);
    byDirectoryCache.remove(v.directory);
    byPathCache.remove(v.path);
  }
}
ログイン後にコピー

We won't be able to understand what's wrong here until we look at all the uses of the byDirectoryCache variable. In all other methods, the interaction occurs in the synchronized block. However, in the removeFromCache method, the programmer removes the collection elements outside of the synchronized blocks.

The analyzer detected this suspicious fragment:

V6102 Inconsistent synchronization of the 'byDirectoryCache' field. Consider synchronizing the field on all usages. CachingDirectoryFactory.java 92, CachingDirectoryFactory.java 228

At this point, one could say there's an error here, and the race condition could happen. However, it turns out that all removeFromCache calls are also enclosed in synchronized blocks. So, this is mostly a false positive that could be suppressed.

Although, we still can enhance this code, because there's a potential issue here. For example, when you need to use this method again, you may simply forget to enclose it in a synchronized block. Even though other methods have additional checks that the object exists in the byDirectoryCache collection, an unsynchronized call may delete an element already after the check. As a result, unnecessary actions are performed in another thread with a non-existent element of the collection, which can lead to errors in the program logic.

To protect ourselves from this, we can simply add the synchronized keyword to the removeFromCache method. So, even though there's no real error here, the static analyzer still urges us to write cleaner code.

By the way, we just recently released an article on the pitfalls of using synchronization.

Is it possible to create a few classes named the same?

In this fragment, the programmer didn't consider that classes can be renamed or declared with the same name in different packages.

private static String getFieldFlags(IndexableField f) {
  IndexOptions opts = (f == null) ? null : f.fieldType().indexOptions();

  StringBuilder flags = new StringBuilder();
  ....
  flags.append((f != null && f.getClass()
                              .getSimpleName()
                              .equals("LazyField"))  // <=
                                   ? FieldFlag.LAZY.getAbbreviation(): '-');
  ....
  return flags.toString();
}
ログイン後にコピー

This is an obviously unnecessary operation that may result in an error. The f variable has the getClass method called, which returns the object type, then gets and checks the name without specifying packages. All in all, there's no error here right now. However, it can arise for two reasons.

The first one is that there may be classes with the same name in different packages. In this case, it's unclear what kind of LazyField is required, and the program will run in a different way than intended.

The second one is related to changing the class name. If the name is changed, the code won't run as intended at all. And searching for all such strings in a huge code base is very difficult. Even if you resort to searching, it's something you can just forget about.

It'd be much safer to use the instanceof operator:

flags.append(f instanceof LazyDocument.LazyField
             ? FieldFlag.LAZY.getAbbreviation(): '-');
ログイン後にコピー

In this case, we wouldn't need to check for null, and the code would be much shorter. The chance of an error would also decrease, if there are classes with the same name in different packages, or if the name of the class changes.

The analyzer detected a potential error and issued a warning:

V6054 Classes should not be compared by their name. LukeRequestHandler.java 247

What about documentation, though?

As a final fragment, we'll look at the following code:

@Override
public UpdateCommand clone() {
  try {
    return (UpdateCommand) super.clone();
  } catch (CloneNotSupportedException e) {
    return null;                         // <=
  }
}
ログイン後にコピー

Let's see what's wrong with it, because everything seems fine at first glance. The analyzer informs us that returning null in the clone method is a bad idea:

V6073 It is not recommended to return null from 'clone' method. UpdateCommand.java 97

Why is it not recommended to return null from clone? It's time to consult the Java documentation:

Returns:

a clone of this instance.

Throws:

CloneNotSupportedException - if the object's class does not support the Cloneable interface. Subclasses that override the clone method can also throw this exception to indicate that an instance cannot be cloned.

The exception here indicates that the object can't be cloned. The method should return only a copy of the current object and nothing else. But why the analyzer doesn't recommend returning null from clone? It's all about further use of the code. If you constantly deviate from the recommendations in the documentation, it's difficult to catch non-standard situations.

Let's imagine a scenario where we want to use the UpdateCommand class, but the source code is unavailable, and we can't decompile it. Or we're just lazy. We can only use the built-in library with this class and focus on the interface. Our program needs us to use the clone method, so we write the following code:

try {
  UpdateCommand localCopy = field.clone(); 
  System.out.println(localCopy.toString();
} catch (CloneNotSupportedException e) {
  System.out.println("Could not clone the field"); 
}
ログイン後にコピー

In this code, we try to catch the CloneNotSupportedException, but we can't because the exception is a NullPointerException that causes the program to crash when calling localCopy.ToString(). This comes as a complete surprise to the developer. Deviating from official recommendations can be annoying, so it's better to always follow them

Conclusion

Let's stop here and take another look at the errors we found. Most of them are the result of carelessness, but there are some that require additional thought. For example, comparing class names without considering packages, or returning null instead of throwing an exception in the clone method.

Without special development tools like static analyzers, such bugs are difficult to find, especially in projects as large as Apache Solr. If you'd like to search for such non-obvious errors in your project, you may try our static analyzer here.

By the way, Solr isn't the only Apache product we checked:

  • 21 bugs in 21st version of Apache NetBeans
  • Big / Bug Data: analyzing the Apache Flink source code
  • Apache Hadoop code quality: production vs test

以上がApache Solr のバグの検索クエリの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!